/**
 * @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 {ElementRef, Injector, QueryList, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, TemplateRef, ViewContainerRef, ViewEncapsulation, getDebugNode} from '@angular/core';
import {getDebugContext} from '@angular/core/src/errors';
import {BindingFlags, DebugContext, NodeDef, NodeFlags, QueryBindingType, QueryValueType, RootData, Services, ViewData, ViewDefinition, ViewDefinitionFactory, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, asProviderData, attachEmbeddedView, detachEmbeddedView, directiveDef, elementDef, queryDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
import {inject} from '@angular/core/testing';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';

import {createRootView} from './helper';

export function main() {
  describe(`Query Views`, () => {
    function compViewDef(
        nodes: NodeDef[], updateDirectives?: ViewUpdateFn, updateRenderer?: ViewUpdateFn,
        viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
      return viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
    }

    function embeddedViewDef(nodes: NodeDef[], update?: ViewUpdateFn): ViewDefinitionFactory {
      return () => viewDef(ViewFlags.None, nodes, update);
    }

    function createAndGetRootNodes(
        viewDef: ViewDefinition, context: any = null): {rootNodes: any[], view: ViewData} {
      const view = createRootView(viewDef, context);
      const rootNodes = rootRenderNodes(view);
      return {rootNodes, view};
    }

    const someQueryId = 1;

    class AService {}

    class QueryService {
      a: QueryList<AService>;
    }

    function contentQueryProviders() {
      return [
        directiveDef(NodeFlags.None, null !, 1, QueryService, []),
        queryDef(
            NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
            {'a': QueryBindingType.All})
      ];
    }

    function compViewQueryProviders(extraChildCount: number, nodes: NodeDef[]) {
      return [
        elementDef(
            NodeFlags.None, null !, null !, 1 + extraChildCount, 'div', null !, null !, null !,
            null !, () => compViewDef([
                      queryDef(
                          NodeFlags.TypeViewQuery | NodeFlags.DynamicQuery, someQueryId,
                          {'a': QueryBindingType.All}),
                      ...nodes
                    ])),
        directiveDef(NodeFlags.Component, null !, 0, QueryService, [], null !, null !, ),
      ];
    }

    function aServiceProvider() {
      return directiveDef(
          NodeFlags.None, [[someQueryId, QueryValueType.Provider]], 0, AService, []);
    }

    describe('content queries', () => {

      it('should query providers on the same element and child elements', () => {
        const {view} = createAndGetRootNodes(compViewDef([
          elementDef(NodeFlags.None, null !, null !, 5, 'div'),
          ...contentQueryProviders(),
          aServiceProvider(),
          elementDef(NodeFlags.None, null !, null !, 1, 'div'),
          aServiceProvider(),
        ]));

        const qs: QueryService = asProviderData(view, 1).instance;
        expect(qs.a).toBeUndefined();

        Services.checkAndUpdateView(view);

        const as = qs.a.toArray();
        expect(as.length).toBe(2);
        expect(as[0]).toBe(asProviderData(view, 3).instance);
        expect(as[1]).toBe(asProviderData(view, 5).instance);
      });

      it('should not query providers on sibling or parent elements', () => {
        const {view} = createAndGetRootNodes(compViewDef([
          elementDef(NodeFlags.None, null !, null !, 6, 'div'),
          aServiceProvider(),
          elementDef(NodeFlags.None, null !, null !, 2, 'div'),
          ...contentQueryProviders(),
          elementDef(NodeFlags.None, null !, null !, 1, 'div'),
          aServiceProvider(),
        ]));

        Services.checkAndUpdateView(view);

        const qs: QueryService = asProviderData(view, 3).instance;
        expect(qs.a.length).toBe(0);
      });
    });

    describe('view queries', () => {
      it('should query providers in the view', () => {
        const {view} = createAndGetRootNodes(compViewDef([
          ...compViewQueryProviders(
              0,
              [
                elementDef(NodeFlags.None, null !, null !, 1, 'span'),
                aServiceProvider(),
              ]),
        ]));

        Services.checkAndUpdateView(view);

        const comp: QueryService = asProviderData(view, 1).instance;
        const compView = asElementData(view, 0).componentView;
        expect(comp.a.length).toBe(1);
        expect(comp.a.first).toBe(asProviderData(compView, 2).instance);
      });

      it('should not query providers on the host element', () => {
        const {view} = createAndGetRootNodes(compViewDef([
          ...compViewQueryProviders(
              1,
              [
                elementDef(NodeFlags.None, null !, null !, 0, 'span'),
              ]),
          aServiceProvider(),
        ]));

        Services.checkAndUpdateView(view);
        const comp: QueryService = asProviderData(view, 1).instance;
        expect(comp.a.length).toBe(0);
      });
    });

    describe('embedded views', () => {
      it('should query providers in embedded views', () => {
        const {view} = createAndGetRootNodes(compViewDef([
          elementDef(NodeFlags.None, null !, null !, 5, 'div'),
          ...contentQueryProviders(),
          anchorDef(NodeFlags.EmbeddedViews, null !, null !, 2, null !, embeddedViewDef([
                      elementDef(NodeFlags.None, null !, null !, 1, 'div'),
                      aServiceProvider(),
                    ])),
          ...contentQueryProviders(),
        ]));

        const childView = Services.createEmbeddedView(view, view.def.nodes[3]);
        attachEmbeddedView(view, asElementData(view, 3), 0, childView);
        Services.checkAndUpdateView(view);

        // queries on parent elements of anchors
        const qs1: QueryService = asProviderData(view, 1).instance;
        expect(qs1.a.length).toBe(1);
        expect(qs1.a.first instanceof AService).toBe(true);

        // queries on the anchor
        const qs2: QueryService = asProviderData(view, 4).instance;
        expect(qs2.a.length).toBe(1);
        expect(qs2.a.first instanceof AService).toBe(true);
      });

      it('should query providers in embedded views only at the template declaration', () => {
        const {view} = createAndGetRootNodes(compViewDef([
          elementDef(NodeFlags.None, null !, null !, 3, 'div'),
          ...contentQueryProviders(),
          anchorDef(NodeFlags.EmbeddedViews, null !, null !, 0, null !, embeddedViewDef([
                      elementDef(NodeFlags.None, null !, null !, 1, 'div'),
                      aServiceProvider(),
                    ])),
          elementDef(NodeFlags.None, null !, null !, 3, 'div'),
          ...contentQueryProviders(),
          anchorDef(NodeFlags.EmbeddedViews, null !, null !, 0),
        ]));

        const childView = Services.createEmbeddedView(view, view.def.nodes[3]);
        // attach at a different place than the one where the template was defined
        attachEmbeddedView(view, asElementData(view, 7), 0, childView);

        Services.checkAndUpdateView(view);

        // query on the declaration place
        const qs1: QueryService = asProviderData(view, 1).instance;
        expect(qs1.a.length).toBe(1);
        expect(qs1.a.first instanceof AService).toBe(true);

        // query on the attach place
        const qs2: QueryService = asProviderData(view, 5).instance;
        expect(qs2.a.length).toBe(0);
      });

      it('should update content queries if embedded views are added or removed', () => {
        const {view} = createAndGetRootNodes(compViewDef([
          elementDef(NodeFlags.None, null !, null !, 3, 'div'),
          ...contentQueryProviders(),
          anchorDef(NodeFlags.EmbeddedViews, null !, null !, 0, null !, embeddedViewDef([
                      elementDef(NodeFlags.None, null !, null !, 1, 'div'),
                      aServiceProvider(),
                    ])),
        ]));

        Services.checkAndUpdateView(view);

        const qs: QueryService = asProviderData(view, 1).instance;
        expect(qs.a.length).toBe(0);

        const childView = Services.createEmbeddedView(view, view.def.nodes[3]);
        attachEmbeddedView(view, asElementData(view, 3), 0, childView);
        Services.checkAndUpdateView(view);

        expect(qs.a.length).toBe(1);

        detachEmbeddedView(asElementData(view, 3), 0);

        Services.checkAndUpdateView(view);

        expect(qs.a.length).toBe(0);
      });

      it('should update view queries if embedded views are added or removed', () => {
        const {view} = createAndGetRootNodes(compViewDef([
          ...compViewQueryProviders(
              0,
              [
                anchorDef(NodeFlags.EmbeddedViews, null !, null !, 0, null !, embeddedViewDef([
                            elementDef(NodeFlags.None, null !, null !, 1, 'div'),
                            aServiceProvider(),
                          ])),
              ]),
        ]));

        Services.checkAndUpdateView(view);

        const comp: QueryService = asProviderData(view, 1).instance;
        expect(comp.a.length).toBe(0);

        const compView = asElementData(view, 0).componentView;
        const childView = Services.createEmbeddedView(compView, compView.def.nodes[1]);
        attachEmbeddedView(view, asElementData(compView, 1), 0, childView);
        Services.checkAndUpdateView(view);

        expect(comp.a.length).toBe(1);

        detachEmbeddedView(asElementData(compView, 1), 0);
        Services.checkAndUpdateView(view);

        expect(comp.a.length).toBe(0);
      });
    });

    describe('QueryBindingType', () => {
      it('should query all matches', () => {
        class QueryService {
          a: QueryList<AService>;
        }

        const {view} = createAndGetRootNodes(compViewDef([
          elementDef(NodeFlags.None, null !, null !, 4, 'div'),
          directiveDef(NodeFlags.None, null !, 1, QueryService, []),
          queryDef(
              NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
              {'a': QueryBindingType.All}),
          aServiceProvider(),
          aServiceProvider(),
        ]));

        Services.checkAndUpdateView(view);

        const qs: QueryService = asProviderData(view, 1).instance;
        expect(qs.a instanceof QueryList).toBeTruthy();
        expect(qs.a.toArray()).toEqual([
          asProviderData(view, 3).instance,
          asProviderData(view, 4).instance,
        ]);
      });

      it('should query the first match', () => {
        class QueryService {
          a: AService;
        }

        const {view} = createAndGetRootNodes(compViewDef([
          elementDef(NodeFlags.None, null !, null !, 4, 'div'),
          directiveDef(NodeFlags.None, null !, 1, QueryService, []),
          queryDef(
              NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
              {'a': QueryBindingType.First}),
          aServiceProvider(),
          aServiceProvider(),
        ]));

        Services.checkAndUpdateView(view);

        const qs: QueryService = asProviderData(view, 1).instance;
        expect(qs.a).toBe(asProviderData(view, 3).instance);
      });
    });

    describe('query builtins', () => {
      it('should query ElementRef', () => {
        class QueryService {
          a: ElementRef;
        }

        const {view} = createAndGetRootNodes(compViewDef([
          elementDef(NodeFlags.None, [[someQueryId, QueryValueType.ElementRef]], null !, 2, 'div'),
          directiveDef(NodeFlags.None, null !, 1, QueryService, []),
          queryDef(
              NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
              {'a': QueryBindingType.First}),
        ]));

        Services.checkAndUpdateView(view);

        const qs: QueryService = asProviderData(view, 1).instance;
        expect(qs.a.nativeElement).toBe(asElementData(view, 0).renderElement);
      });

      it('should query TemplateRef', () => {
        class QueryService {
          a: TemplateRef<any>;
        }

        const {view} = createAndGetRootNodes(compViewDef([
          anchorDef(
              NodeFlags.None, [[someQueryId, QueryValueType.TemplateRef]], null !, 2, null !,
              embeddedViewDef([anchorDef(NodeFlags.None, null !, null !, 0)])),
          directiveDef(NodeFlags.None, null !, 1, QueryService, []),
          queryDef(
              NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
              {'a': QueryBindingType.First}),
        ]));

        Services.checkAndUpdateView(view);

        const qs: QueryService = asProviderData(view, 1).instance;
        expect(qs.a.createEmbeddedView).toBeTruthy();
      });

      it('should query ViewContainerRef', () => {
        class QueryService {
          a: ViewContainerRef;
        }

        const {view} = createAndGetRootNodes(compViewDef([
          anchorDef(
              NodeFlags.EmbeddedViews, [[someQueryId, QueryValueType.ViewContainerRef]], null !, 2),
          directiveDef(NodeFlags.None, null !, 1, QueryService, []),
          queryDef(
              NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
              {'a': QueryBindingType.First}),
        ]));

        Services.checkAndUpdateView(view);

        const qs: QueryService = asProviderData(view, 1).instance;
        expect(qs.a.createEmbeddedView).toBeTruthy();
      });
    });

    describe('general binding behavior', () => {
      it('should report debug info on binding errors', () => {
        class QueryService {
          set a(value: any) { throw new Error('Test'); }
        }

        const {view} = createAndGetRootNodes(compViewDef([
          elementDef(NodeFlags.None, null !, null !, 3, 'div'),
          directiveDef(NodeFlags.None, null !, 1, QueryService, []),
          queryDef(
              NodeFlags.TypeContentQuery | NodeFlags.DynamicQuery, someQueryId,
              {'a': QueryBindingType.All}),
          aServiceProvider(),
        ]));


        let err: any;
        try {
          Services.checkAndUpdateView(view);
        } catch (e) {
          err = e;
        }
        expect(err).toBeTruthy();
        expect(err.message).toBe('Test');
        const debugCtx = getDebugContext(err);
        expect(debugCtx.view).toBe(view);
        expect(debugCtx.nodeIndex).toBe(2);
      });
    });
  });
}
