/**
 * @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 {
  Component,
  computed,
  contentChild,
  contentChildren,
  createComponent,
  Directive,
  ElementRef,
  EnvironmentInjector,
  QueryList,
  viewChild,
  ViewChildren,
  viewChildren,
} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';

describe('queries as signals', () => {
  describe('view', () => {
    it('should query for an optional element in a template', () => {
      @Component({
        standalone: true,
        template: `<div #el></div>`,
      })
      class AppComponent {
        divEl = viewChild<ElementRef<HTMLDivElement>>('el');
        foundEl = computed(() => this.divEl() != null);
      }

      const fixture = TestBed.createComponent(AppComponent);
      // with signal based queries we _do_ have query results after the creation mode
      // execution
      // (before the change detection runs) so we can return those early on! In this sense all
      // queries behave as "static" (?)
      expect(fixture.componentInstance.foundEl()).toBeTrue();

      fixture.detectChanges();
      expect(fixture.componentInstance.foundEl()).toBeTrue();
    });

    it('should return undefined if optional query is read in the constructor', () => {
      let result: {} | undefined = {};

      @Component({
        standalone: true,
        template: `<div #el></div>`,
      })
      class AppComponent {
        divEl = viewChild<ElementRef<HTMLDivElement>>('el');

        constructor() {
          result = this.divEl();
        }
      }

      TestBed.createComponent(AppComponent);
      expect(result).toBeUndefined();
    });

    it('should query for a required element in a template', () => {
      @Component({
        standalone: true,
        template: `<div #el></div>`,
      })
      class AppComponent {
        divEl = viewChild.required<ElementRef<HTMLDivElement>>('el');
        foundEl = computed(() => this.divEl() != null);
      }

      const fixture = TestBed.createComponent(AppComponent);
      // with signal based queries we _do_ have query results after the creation mode execution
      // (before the change detection runs) so we can return those early on! In this sense all
      // queries behave as "static" (?)
      expect(fixture.componentInstance.foundEl()).toBeTrue();

      fixture.detectChanges();
      expect(fixture.componentInstance.foundEl()).toBeTrue();
    });

    it('should throw if required query is read in the constructor', () => {
      @Component({
        standalone: true,
        template: `<div #el></div>`,
      })
      class AppComponent {
        divEl = viewChild.required<ElementRef<HTMLDivElement>>('el');

        constructor() {
          this.divEl();
        }
      }

      // non-required query results are undefined before we run creation mode on the view queries
      expect(() => {
        TestBed.createComponent(AppComponent);
      }).toThrowError(/NG0951: Child query result is required but no value is available/);
    });

    it('should query for multiple elements in a template', () => {
      @Component({
        standalone: true,
        template: `
          <div #el></div>
          @if (show) {
            <div #el></div>
          }
        `,
      })
      class AppComponent {
        show = false;

        divEls = viewChildren<ElementRef<HTMLDivElement>>('el');
        foundEl = computed(() => this.divEls().length);
      }

      const fixture = TestBed.createComponent(AppComponent);
      // with signal based queries we _do_ have query results after the creation mode execution
      // (before the change detection runs) so we can return those early on! In this sense all
      // queries behave as "static" (?)
      expect(fixture.componentInstance.foundEl()).toBe(1);

      fixture.detectChanges();
      expect(fixture.componentInstance.foundEl()).toBe(1);

      fixture.componentInstance.show = true;
      fixture.detectChanges();
      expect(fixture.componentInstance.foundEl()).toBe(2);

      fixture.componentInstance.show = false;
      fixture.detectChanges();
      expect(fixture.componentInstance.foundEl()).toBe(1);
    });

    it('should return an empty array when reading children query in the constructor', () => {
      let result: readonly ElementRef[] | undefined;

      @Component({
        standalone: true,
        template: `<div #el></div>`,
      })
      class AppComponent {
        divEls = viewChildren<ElementRef<HTMLDivElement>>('el');

        constructor() {
          result = this.divEls();
        }
      }

      TestBed.createComponent(AppComponent);
      expect(result).toEqual([]);
    });

    it('should return the same array instance when there were no changes in results', () => {
      @Component({
        standalone: true,
        template: `<div #el></div>`,
      })
      class AppComponent {
        divEls = viewChildren<ElementRef<HTMLDivElement>>('el');
      }

      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      const result1 = fixture.componentInstance.divEls();
      expect(result1.length).toBe(1);

      // subsequent reads should return the same result instance
      const result2 = fixture.componentInstance.divEls();
      expect(result2.length).toBe(1);
      expect(result2).toBe(result1);
    });

    it('should not mark signal as dirty when a child query result does not change', () => {
      let computeCount = 0;

      @Component({
        standalone: true,
        template: `
            <div #el></div>
            @if (show) {
              <div #el></div>
            }
          `,
      })
      class AppComponent {
        divEl = viewChild.required<ElementRef<HTMLDivElement>>('el');
        isThere = computed(() => ++computeCount);
        show = false;
      }

      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      expect(fixture.componentInstance.isThere()).toBe(1);
      const divEl = fixture.componentInstance.divEl();

      // subsequent reads should return the same result instance and _not_ trigger downstream
      // computed re-evaluation
      fixture.componentInstance.show = true;
      fixture.detectChanges();
      expect(fixture.componentInstance.divEl()).toBe(divEl);
      expect(fixture.componentInstance.isThere()).toBe(1);
    });

    it('should return the same array instance when there were no changes in results after view manipulation', () => {
      @Component({
        standalone: true,
        template: `
            <div #el></div>
            @if (show) {
              <div></div>
            }
          `,
      })
      class AppComponent {
        divEls = viewChildren<ElementRef<HTMLDivElement>>('el');

        show = false;
      }

      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      const result1 = fixture.componentInstance.divEls();
      expect(result1.length).toBe(1);

      fixture.componentInstance.show = true;
      fixture.detectChanges();
      // subsequent reads should return the same result instance since the query results didn't
      // change
      const result2 = fixture.componentInstance.divEls();
      expect(result2.length).toBe(1);
      expect(result2).toBe(result1);
    });

    it('should be empty when no query matches exist', () => {
      @Component({
        standalone: true,
        template: ``,
      })
      class AppComponent {
        result = viewChild('unknown');
        results = viewChildren('unknown');
      }

      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();

      expect(fixture.componentInstance.result()).toBeUndefined();
      expect(fixture.componentInstance.results().length).toBe(0);
    });
  });

  describe('content queries', () => {
    it('should run content queries defined on components', () => {
      @Component({
        selector: 'query-cmp',
        standalone: true,
        template: `{{noOfEls()}}`,
      })
      class QueryComponent {
        elements = contentChildren('el');
        element = contentChild('el');
        elementReq = contentChild.required('el');

        noOfEls = computed(
          () =>
            this.elements().length +
            (this.element() !== undefined ? 1 : 0) +
            (this.elementReq() !== undefined ? 1 : 0),
        );
      }

      @Component({
        standalone: true,
        imports: [QueryComponent],
        template: `
          <query-cmp>
            <div #el></div >
            @if (show) {
              <div #el></div>
            }
          </query-cmp>
        `,
      })
      class AppComponent {
        show = false;
      }

      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('3');

      fixture.componentInstance.show = true;
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('4');

      fixture.componentInstance.show = false;
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('3');
    });

    it('should run content queries defined on directives', () => {
      @Directive({
        selector: '[query]',
        standalone: true,
        host: {'[textContent]': `noOfEls()`},
      })
      class QueryDir {
        elements = contentChildren('el');
        element = contentChild('el');
        elementReq = contentChild.required('el');

        noOfEls = computed(
          () =>
            this.elements().length +
            (this.element() !== undefined ? 1 : 0) +
            (this.elementReq() !== undefined ? 1 : 0),
        );
      }

      @Component({
        standalone: true,
        imports: [QueryDir],
        template: `
          <div query>
            <div #el></div>
            @if (show) {
              <div #el></div>
            }
          </div>
        `,
      })
      class AppComponent {
        show = false;
      }

      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('3');

      fixture.componentInstance.show = true;
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('4');

      fixture.componentInstance.show = false;
      fixture.detectChanges();
      expect(fixture.nativeElement.textContent).toBe('3');
    });

    it('should not return partial results during the first-time view rendering', () => {
      @Directive({selector: '[marker]', standalone: true})
      class MarkerForResults {}

      @Directive({
        selector: '[declare]',
        standalone: true,
      })
      class DeclareQuery {
        results = contentChildren(MarkerForResults);
      }

      @Directive({selector: '[inspect]', standalone: true})
      class InspectsQueryResults {
        constructor(declaration: DeclareQuery) {
          // we should _not_ get partial query results while the view is still creating
          expect(declaration.results().length).toBe(0);
        }
      }

      @Component({
        standalone: true,
        imports: [MarkerForResults, InspectsQueryResults, DeclareQuery],
        template: `
                <div declare>
                  <div marker></div>
                  <div inspect></div>
                  <div marker></div>
                </div>
             `,
      })
      class AppComponent {}

      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      const queryDir = fixture.debugElement
        .query(By.directive(DeclareQuery))
        .injector.get(DeclareQuery);

      expect(queryDir.results().length).toBe(2);
    });

    it('should be empty when no query matches exist', () => {
      @Directive({
        selector: '[declare]',
        standalone: true,
      })
      class DeclareQuery {
        result = contentChild('unknown');
        results = contentChildren('unknown');
      }

      @Component({
        standalone: true,
        imports: [DeclareQuery],
        template: `<div declare></div>`,
      })
      class AppComponent {}

      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      const queryDir = fixture.debugElement
        .query(By.directive(DeclareQuery))
        .injector.get(DeclareQuery);

      expect(queryDir.result()).toBeUndefined();
      expect(queryDir.results().length).toBe(0);
    });
  });

  describe('reactivity and performance', () => {
    it('should not dirty a children query when a list of matches does not change - a view with matches', () => {
      let recomputeCount = 0;

      @Component({
        standalone: true,
        template: `
          <div #el></div>
          @if (show) {
            <div #el></div>
          }
        `,
      })
      class AppComponent {
        divEls = viewChildren<ElementRef<HTMLDivElement>>('el');
        foundElCount = computed(() => {
          recomputeCount++;
          return this.divEls().length;
        });
        show = false;
      }

      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      expect(fixture.componentInstance.foundElCount()).toBe(1);
      expect(recomputeCount).toBe(1);

      // trigger view manipulation that should dirty queries but not change the results
      fixture.componentInstance.show = true;
      fixture.detectChanges();
      fixture.componentInstance.show = false;
      fixture.detectChanges();

      expect(fixture.componentInstance.foundElCount()).toBe(1);
      expect(recomputeCount).toBe(1);
    });

    it('should not dirty a children query when a list of matches does not change - a view with another container', () => {
      let recomputeCount = 0;

      @Component({
        standalone: true,
        template: `
          <div #el></div>
          @if (show) {
            <!-- an empty if to create a container -->
            @if (true) {}
          }
        `,
      })
      class AppComponent {
        divEls = viewChildren<ElementRef<HTMLDivElement>>('el');
        foundElCount = computed(() => {
          recomputeCount++;
          return this.divEls().length;
        });
        show = false;
      }

      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      expect(fixture.componentInstance.foundElCount()).toBe(1);
      expect(recomputeCount).toBe(1);

      // trigger view manipulation that should dirty queries but not change the results
      fixture.componentInstance.show = true;
      fixture.detectChanges();
      fixture.componentInstance.show = false;
      fixture.detectChanges();

      expect(fixture.componentInstance.foundElCount()).toBe(1);
      expect(recomputeCount).toBe(1);
    });
  });

  describe('dynamic component creation', () => {
    it('should return empty results for content queries of dynamically created components', () => {
      // https://github.com/angular/angular/issues/54450
      @Component({
        selector: 'query-cmp',
        standalone: true,
        template: ``,
      })
      class QueryComponent {
        elements = contentChildren('el');
        element = contentChild('el');
      }

      @Component({
        standalone: true,
        template: ``,
      })
      class TestComponent {
        constructor(private _envInjector: EnvironmentInjector) {}

        createDynamic() {
          return createComponent(QueryComponent, {environmentInjector: this._envInjector});
        }
      }

      const fixture = TestBed.createComponent(TestComponent);
      const cmpRef = fixture.componentInstance.createDynamic();
      cmpRef.changeDetectorRef.detectChanges();

      expect(cmpRef.instance.elements()).toEqual([]);
      expect(cmpRef.instance.element()).toBeUndefined();
    });
  });

  describe('mix of signal and decorator queries', () => {
    it('should allow specifying both types of queries in one component', () => {
      @Component({
        standalone: true,
        template: `
          <div #el></div>
          @if (show) {
            <div #el></div>
          }
        `,
      })
      class AppComponent {
        show = false;

        divElsSignal = viewChildren<ElementRef<HTMLDivElement>>('el');

        @ViewChildren('el') divElsDecorator!: QueryList<ElementRef<HTMLDivElement>>;
      }

      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      expect(fixture.componentInstance.divElsSignal().length).toBe(1);
      expect(fixture.componentInstance.divElsDecorator.length).toBe(1);

      fixture.componentInstance.show = true;
      fixture.detectChanges();
      expect(fixture.componentInstance.divElsSignal().length).toBe(2);
      expect(fixture.componentInstance.divElsDecorator.length).toBe(2);

      fixture.componentInstance.show = false;
      fixture.detectChanges();
      expect(fixture.componentInstance.divElsSignal().length).toBe(1);
      expect(fixture.componentInstance.divElsDecorator.length).toBe(1);
    });

    it('should allow combination via inheritance of both types of queries in one component', () => {
      @Component({
        standalone: true,
        template: `
            <div #el></div>
            @if (show) {
              <div #el></div>
            }
          `,
      })
      class BaseComponent {
        show = false;
        divElsSignal = viewChildren<ElementRef<HTMLDivElement>>('el');
      }

      @Component({
        standalone: true,
        template: `
            <div #el></div>
            @if (show) {
              <div #el></div>
            }
          `,
      })
      class AppComponent extends BaseComponent {
        @ViewChildren('el') divElsDecorator!: QueryList<ElementRef<HTMLDivElement>>;
      }

      const fixture = TestBed.createComponent(AppComponent);
      fixture.detectChanges();
      expect(fixture.componentInstance.divElsSignal().length).toBe(1);
      expect(fixture.componentInstance.divElsDecorator.length).toBe(1);

      fixture.componentInstance.show = true;
      fixture.detectChanges();
      expect(fixture.componentInstance.divElsSignal().length).toBe(2);
      expect(fixture.componentInstance.divElsDecorator.length).toBe(2);

      fixture.componentInstance.show = false;
      fixture.detectChanges();
      expect(fixture.componentInstance.divElsSignal().length).toBe(1);
      expect(fixture.componentInstance.divElsDecorator.length).toBe(1);
    });
  });
});
