import {Component, signal} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ComponentHarness, HarnessLoader, HarnessPredicate, parallel} from '@angular/cdk/testing';
import {createFakeEvent, dispatchFakeEvent} from '@angular/cdk/testing/private';
import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed';
import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
import {
  MatError,
  MatFormField,
  MatHint,
  MatLabel,
  MatPrefix,
  MatSuffix,
} from '@angular/material/form-field';
import {MatAutocomplete, MatAutocompleteTrigger} from '@angular/material/autocomplete';
import {MatInput} from '@angular/material/input';
import {MatSelect} from '@angular/material/select';
import {MatNativeDateModule, MatOption} from '@angular/material/core';
import {
  MatDateRangeInput,
  MatDateRangePicker,
  MatDatepicker,
  MatDatepickerInput,
  MatDatepickerModule,
  MatEndDate,
  MatStartDate,
} from '@angular/material/datepicker';
import {MatInputHarness} from '@angular/material/input/testing';
import {MatSelectHarness} from '@angular/material/select/testing';
import {
  MatDateRangeInputHarness,
  MatDatepickerInputHarness,
} from '@angular/material/datepicker/testing';
import {MatFormFieldHarness} from './form-field-harness';
import {MatErrorHarness} from './error-harness';

describe('MatFormFieldHarness', () => {
  let fixture: ComponentFixture<FormFieldHarnessTest>;
  let loader: HarnessLoader;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        NoopAnimationsModule,
        MatNativeDateModule,
        FormFieldHarnessTest,
        MatDatepickerModule,
      ],
    });

    fixture = TestBed.createComponent(FormFieldHarnessTest);
    fixture.detectChanges();
    loader = TestbedHarnessEnvironment.loader(fixture);
  });

  it('should be able to load harnesses', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(formFields.length).toBe(7);
  });

  it('should be able to load form-field that matches specific selector', async () => {
    const formFieldMatches = await loader.getAllHarnesses(
      MatFormFieldHarness.with({
        selector: '#first-form-field',
      }),
    );
    expect(formFieldMatches.length).toBe(1);
  });

  it('should be able to get appearance of form-field', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[0].getAppearance()).toBe('fill');
    expect(await formFields[1].getAppearance()).toBe('fill');
    expect(await formFields[2].getAppearance()).toBe('fill');
    expect(await formFields[3].getAppearance()).toBe('outline');
    expect(await formFields[4].getAppearance()).toBe('fill');
  });

  it('should be able to get control of form-field', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect((await formFields[0].getControl()) instanceof MatInputHarness).toBe(true);
    expect((await formFields[1].getControl()) instanceof MatInputHarness).toBe(true);
    expect((await formFields[2].getControl()) instanceof MatSelectHarness).toBe(true);
    expect((await formFields[3].getControl()) instanceof MatInputHarness).toBe(true);
    expect((await formFields[4].getControl()) instanceof MatInputHarness).toBe(true);
    expect((await formFields[5].getControl()) instanceof MatDatepickerInputHarness).toBe(true);
    expect((await formFields[6].getControl()) instanceof MatDateRangeInputHarness).toBe(true);
  });

  it('should be able to get custom control of form-field', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[0].getControl(CustomControlHarness)).toBe(null);
    expect(
      (await formFields[1].getControl(CustomControlHarness)) instanceof CustomControlHarness,
    ).toBe(true);
    expect(await formFields[2].getControl(CustomControlHarness)).toBe(null);
    expect(await formFields[3].getControl(CustomControlHarness)).toBe(null);
    expect(await formFields[4].getControl(CustomControlHarness)).toBe(null);
  });

  it('should be able to get custom control of form-field using a predicate', async () => {
    const predicate = new HarnessPredicate(CustomControlHarness, {});
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[0].getControl(predicate)).toBe(null);
    expect((await formFields[1].getControl(predicate)) instanceof CustomControlHarness).toBe(true);
    expect(await formFields[2].getControl(predicate)).toBe(null);
    expect(await formFields[3].getControl(predicate)).toBe(null);
    expect(await formFields[4].getControl(predicate)).toBe(null);
  });

  it('should be able to check whether form-field has label', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[0].hasLabel()).toBe(false);
    expect(await formFields[1].hasLabel()).toBe(false);
    expect(await formFields[2].hasLabel()).toBe(true);
    expect(await formFields[3].hasLabel()).toBe(true);
    expect(await formFields[4].hasLabel()).toBe(true);
  });

  it('should be able to check whether label is floating', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[0].isLabelFloating()).toBe(false);
    expect(await formFields[1].isLabelFloating()).toBe(false);
    expect(await formFields[2].isLabelFloating()).toBe(false);
    expect(await formFields[3].isLabelFloating()).toBe(true);
    expect(await formFields[4].isLabelFloating()).toBe(false);

    fixture.componentInstance.shouldLabelFloat.set('always');
    expect(await formFields[4].isLabelFloating()).toBe(true);
  });

  it('should be able to check whether form-field is disabled', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[0].isDisabled()).toBe(false);
    expect(await formFields[1].isDisabled()).toBe(false);
    expect(await formFields[2].isDisabled()).toBe(false);
    expect(await formFields[3].isDisabled()).toBe(false);
    expect(await formFields[4].isDisabled()).toBe(false);

    fixture.componentInstance.isDisabled.set(true);
    expect(await formFields[0].isDisabled()).toBe(true);
    expect(await formFields[1].isDisabled()).toBe(false);
    expect(await formFields[2].isDisabled()).toBe(true);
    expect(await formFields[3].isDisabled()).toBe(false);
    expect(await formFields[4].isDisabled()).toBe(false);
  });

  it('should be able to check whether form-field is auto-filled', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[0].isAutofilled()).toBe(false);
    expect(await formFields[1].isAutofilled()).toBe(false);
    expect(await formFields[2].isAutofilled()).toBe(false);
    expect(await formFields[3].isAutofilled()).toBe(false);
    expect(await formFields[4].isAutofilled()).toBe(false);

    const autofillTriggerEvent: any = createFakeEvent('animationstart');
    autofillTriggerEvent.animationName = 'cdk-text-field-autofill-start';

    // Dispatch an "animationstart" event on the input to trigger the
    // autofill monitor.
    fixture.nativeElement
      .querySelector('#first-form-field input')
      .dispatchEvent(autofillTriggerEvent);

    expect(await formFields[0].isAutofilled()).toBe(true);
    expect(await formFields[1].isAutofilled()).toBe(false);
    expect(await formFields[2].isAutofilled()).toBe(false);
    expect(await formFields[3].isAutofilled()).toBe(false);
    expect(await formFields[4].isAutofilled()).toBe(false);
  });

  it('should be able to get theme color of form-field', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[0].getThemeColor()).toBe('primary');
    expect(await formFields[1].getThemeColor()).toBe('warn');
    expect(await formFields[2].getThemeColor()).toBe('accent');
    expect(await formFields[3].getThemeColor()).toBe('primary');
    expect(await formFields[4].getThemeColor()).toBe('primary');
  });

  it('should be able to get label of form-field', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[0].getLabel()).toBe(null);
    expect(await formFields[1].getLabel()).toBe(null);
    expect(await formFields[2].getLabel()).toBe('Label');
    expect(await formFields[3].getLabel()).toBe('autocomplete_label');
    expect(await formFields[4].getLabel()).toBe('Label');
  });

  it('should be able to get error messages of form-field', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[1].getTextErrors()).toEqual([]);

    fixture.componentInstance.requiredControl.setValue('');
    dispatchFakeEvent(fixture.nativeElement.querySelector('#with-errors input'), 'blur');
    expect(await formFields[1].getTextErrors()).toEqual(['Error 1', 'Error 2']);
  });

  it('should be able to get form-field by validity', async () => {
    let invalid = await loader.getAllHarnesses(MatFormFieldHarness.with({isValid: false}));
    expect(invalid.length).toBe(0);

    fixture.componentInstance.requiredControl.setValue('');
    dispatchFakeEvent(fixture.nativeElement.querySelector('#with-errors input'), 'blur');

    invalid = await loader.getAllHarnesses(MatFormFieldHarness.with({isValid: false}));
    expect(invalid.length).toBe(1);
  });

  it('should be able to get error harnesses from the form-field harness', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[1].getErrors()).toEqual([]);

    fixture.componentInstance.requiredControl.setValue('');
    dispatchFakeEvent(fixture.nativeElement.querySelector('#with-errors input'), 'blur');

    const formFieldErrorHarnesses = await formFields[1].getErrors();
    expect(formFieldErrorHarnesses.length).toBe(2);
    expect(await formFieldErrorHarnesses[0].getText()).toBe('Error 1');
    expect(await formFieldErrorHarnesses[1].getText()).toBe('Error 2');
    const error1Harnesses = await formFields[1].getErrors({text: 'Error 1'});
    expect(error1Harnesses.length).toBe(1);
    expect(await error1Harnesses[0].getText()).toBe('Error 1');
  });

  it('should be able to directly load error harnesses', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[1].getErrors()).toEqual([]);

    fixture.componentInstance.requiredControl.setValue('');
    dispatchFakeEvent(fixture.nativeElement.querySelector('#with-errors input'), 'blur');

    const errorHarnesses = await loader.getAllHarnesses(MatErrorHarness);
    expect(errorHarnesses.length).toBe(2);
    expect(await errorHarnesses[0].getText()).toBe('Error 1');
    expect(await errorHarnesses[1].getText()).toBe('Error 2');
    const error1Harnesses = await loader.getAllHarnesses(MatErrorHarness.with({text: 'Error 1'}));
    expect(error1Harnesses.length).toBe(1);
    expect(await error1Harnesses[0].getText()).toBe('Error 1');
  });

  it('should be able to get hint messages of form-field', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[1].getTextHints()).toEqual(['Hint 1', 'Hint 2']);

    fixture.componentInstance.requiredControl.setValue('');
    dispatchFakeEvent(fixture.nativeElement.querySelector('#with-errors input'), 'blur');
    expect(await formFields[1].getTextHints()).toEqual([]);
  });

  it('should be able to get the prefix text of a form-field', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    const prefixTexts = await parallel(() => formFields.map(f => f.getPrefixText()));
    expect(prefixTexts).toEqual(['prefix_textprefix_text_2', '', '', '', '', '', '']);
  });

  it('should be able to get the suffix text of a form-field', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    const suffixTexts = await parallel(() => formFields.map(f => f.getSuffixText()));
    expect(suffixTexts).toEqual(['suffix_text', '', '', '', '', '', '']);
  });

  it('should be able to check if form field has been touched', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[0].isControlTouched()).toBe(null);
    expect(await formFields[1].isControlTouched()).toBe(false);

    fixture.componentInstance.requiredControl.setValue('');
    dispatchFakeEvent(fixture.nativeElement.querySelector('#with-errors input'), 'blur');
    expect(await formFields[1].isControlTouched()).toBe(true);
  });

  it('should be able to check if form field is invalid', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[0].isControlValid()).toBe(null);
    expect(await formFields[1].isControlValid()).toBe(true);

    fixture.componentInstance.requiredControl.setValue('');
    expect(await formFields[1].isControlValid()).toBe(false);
  });

  it('should be able to check if form field is dirty', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[0].isControlDirty()).toBe(null);
    expect(await formFields[1].isControlDirty()).toBe(false);

    fixture.componentInstance.requiredControl.setValue('new value');
    dispatchFakeEvent(fixture.nativeElement.querySelector('#with-errors input'), 'input');
    expect(await formFields[1].isControlDirty()).toBe(true);
  });

  it('should be able to check if form field is pending async validation', async () => {
    const formFields = await loader.getAllHarnesses(MatFormFieldHarness);
    expect(await formFields[0].isControlPending()).toBe(null);
    expect(await formFields[1].isControlPending()).toBe(false);

    fixture.componentInstance.setupAsyncValidator();
    fixture.componentInstance.requiredControl.setValue('');
    expect(await formFields[1].isControlPending()).toBe(true);
  });
});

@Component({
  template: `
    <mat-form-field id="first-form-field" [floatLabel]="shouldLabelFloat()">
      <span matTextPrefix>prefix_text</span>
      <span matTextPrefix>prefix_text_2</span>
      <input matInput value="Sushi" name="favorite-food" placeholder="With placeholder"
             [disabled]="isDisabled()">
      <span matTextSuffix>suffix_text</span>
    </mat-form-field>

    <mat-form-field appearance="fill" color="warn" id="with-errors">
      <span class="custom-control">Custom control harness</span>
      <input matInput [formControl]="requiredControl">

      <mat-error>Error 1</mat-error>
      <div matError>Error 2</div>
      <mat-hint align="start">Hint 1</mat-hint>
      <mat-hint align="end">Hint 2</mat-hint>
    </mat-form-field>

    <mat-form-field appearance="fill" color="accent">
      <mat-label>Label</mat-label>
      <mat-select [disabled]="isDisabled()">
        <mat-option>First</mat-option>
      </mat-select>
    </mat-form-field>

    <mat-form-field floatLabel="always" appearance="outline" color="primary">
      <mat-label>autocomplete_label</mat-label>
      <input type="text" matInput [matAutocomplete]="auto">
    </mat-form-field>

    <mat-autocomplete #auto="matAutocomplete">
      <mat-option>autocomplete_option</mat-option>
    </mat-autocomplete>

    <mat-form-field id="last-form-field" [floatLabel]="shouldLabelFloat()">
      <mat-label>Label</mat-label>
      <input matInput>
    </mat-form-field>

    <mat-form-field>
      <mat-label>Date</mat-label>
      <input matInput [matDatepicker]="datepicker">
      <mat-datepicker #datepicker></mat-datepicker>
    </mat-form-field>

    <mat-form-field>
      <mat-label>Date range</mat-label>
      <mat-date-range-input [rangePicker]="rangePicker">
        <input matStartDate placeholder="Start date"/>
        <input matEndDate placeholder="End date"/>
      </mat-date-range-input>
      <mat-date-range-picker #rangePicker></mat-date-range-picker>
    </mat-form-field>
  `,
  standalone: true,
  imports: [
    ReactiveFormsModule,
    MatNativeDateModule,
    MatAutocomplete,
    MatAutocompleteTrigger,
    MatDatepicker,
    MatDatepickerInput,
    MatDateRangePicker,
    MatDateRangeInput,
    MatEndDate,
    MatError,
    MatFormField,
    MatHint,
    MatInput,
    MatLabel,
    MatPrefix,
    MatSelect,
    MatStartDate,
    MatSuffix,
    MatOption,
  ],
})
class FormFieldHarnessTest {
  requiredControl = new FormControl('Initial value', [Validators.required]);
  shouldLabelFloat = signal<'always' | 'auto'>('auto');
  hasLabel = false;
  isDisabled = signal(false);

  setupAsyncValidator() {
    this.requiredControl.setValidators(() => null);
    this.requiredControl.setAsyncValidators(() => new Promise(res => setTimeout(res, 10000)));
  }
}

class CustomControlHarness extends ComponentHarness {
  static hostSelector = '.custom-control';
}
