import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import {
  TextInputDialogComponent,
  TextInputDialogData,
} from './text-input-dialog.component';
import { DialogRef, DIALOG_DATA } from '@angular/cdk/dialog';
import { ButtonComponent } from '@isa/ui/buttons';
import {
  InputControlDirective,
  TextFieldClearComponent,
  TextFieldComponent,
  TextFieldContainerComponent,
  TextFieldErrorsComponent,
} from '@isa/ui/input-controls';
import { DialogComponent } from '../dialog.component';
import { ScannerService } from '@isa/shared/scanner';
import { CONFIG_DATA } from '@isa/core/config';
import { signal } from '@angular/core';

// Mock Scandit modules to avoid loading external dependencies
jest.mock('scandit-web-datacapture-core', () => ({}));
jest.mock('scandit-web-datacapture-barcode', () => ({}));

describe('TextInputDialogComponent', () => {
  let spectator: Spectator<TextInputDialogComponent>;
  let mockDialogRef: DialogRef<any>;
  let mockScannerService: jest.Mocked<ScannerService>;
  let mockData: TextInputDialogData;

  const createComponent = createComponentFactory({
    component: TextInputDialogComponent,
    mocks: [ScannerService],
    detectChanges: false,
    providers: [
      { provide: DialogRef, useValue: { close: jest.fn() } },
      {
        provide: DIALOG_DATA,
        useFactory: () => mockData,
      },
      { provide: DialogComponent, useValue: {} },
      { provide: CONFIG_DATA, useValue: {} },
    ],
  });

  beforeEach(() => {
    // Reset mockData before each test
    mockData = {
      message: 'Please enter your name',
      inputLabel: 'Name',
      inputDefaultValue: 'John Doe',
      closeText: 'Cancel',
      confirmText: 'Save',
    };

    spectator = createComponent();
    mockDialogRef = spectator.inject(DialogRef);
    mockScannerService = spectator.inject(ScannerService);
    // Add ready signal to mocked scanner service
    (mockScannerService as any).ready = signal(true);
    jest.clearAllMocks();
    spectator.detectChanges();
  });

  it('should create', () => {
    expect(spectator.component).toBeTruthy();
  });

  it('should render the dialog message', () => {
    expect(spectator.query('[data-which="dialog-message"]')).toHaveText(
      'Please enter your name',
    );
  });

  it('should render the scanner button', () => {
    const scannerButton = spectator.query('shared-scanner-button');
    expect(scannerButton).toBeTruthy();
  });

  it('should update the control value when input changes', () => {
    const input = spectator.query('input') as HTMLInputElement;
    spectator.typeInElement('Jane', input);
    spectator.detectChanges();
    expect(spectator.component.control.value).toBe('Jane');
  });

  it('should update control value when onScan is called', () => {
    const scannedValue = '1234567890';
    spectator.component.onScan(scannedValue);
    expect(spectator.component.control.value).toBe(scannedValue);
  });

  it('should disable scanner button when input has value', () => {
    spectator.component.control.setValue('Some value');
    spectator.detectChanges();
    const scannerButton = spectator.query('shared-scanner-button');
    expect(scannerButton).toBeTruthy();
    // The button component binds disabled input [disabled]="!!control?.value"
    // Since control.value is truthy, disabled should be true
    const debugElement = spectator.debugElement.query(
      (el) => el.nativeElement.tagName === 'SHARED-SCANNER-BUTTON',
    );
    // disabled is a signal model, need to call it as a function
    expect(debugElement?.componentInstance.disabled()).toBe(true);
  });

  it('should close dialog with scanned value when save button is clicked after scanning', () => {
    const scannedValue = 'SCAN123456';

    spectator.component.onScan(scannedValue);
    spectator.detectChanges();

    const saveButton = spectator.query(
      '[data-which="save"]',
    ) as HTMLButtonElement;
    spectator.click(saveButton);

    expect(mockDialogRef.close).toHaveBeenCalledWith({
      inputValue: scannedValue,
    });
  });

  describe('onConfirm with async callback', () => {
    it('should execute onConfirm callback and close dialog on success', async () => {
      const mockOnConfirm = jest.fn().mockResolvedValue(undefined);
      mockData.onConfirm = mockOnConfirm;
      spectator.component.control.setValue('test-value');
      spectator.detectChanges();

      await spectator.component.onConfirm();

      expect(mockOnConfirm).toHaveBeenCalledWith('test-value');
      expect(mockDialogRef.close).toHaveBeenCalledWith({
        inputValue: 'test-value',
      });
      expect(spectator.component.asyncError()).toBeNull();
    });

    it('should set isLoading to true during async operation', async () => {
      let resolveCallback: () => void;
      const mockOnConfirm = jest.fn(
        () =>
          new Promise<void>((resolve) => {
            resolveCallback = resolve;
          }),
      );
      mockData.onConfirm = mockOnConfirm;
      spectator.component.control.setValue('test-value');

      const confirmPromise = spectator.component.onConfirm();

      // Should be loading during the operation
      expect(spectator.component.isLoading()).toBe(true);

      resolveCallback!();
      await confirmPromise;

      // After success, dialog is closed so we don't test isLoading state
      expect(mockDialogRef.close).toHaveBeenCalled();
    });

    it('should set asyncError and keep dialog open on callback failure', async () => {
      const errorMessage = 'Backend validation failed';
      const mockOnConfirm = jest
        .fn()
        .mockRejectedValue(new Error(errorMessage));
      mockData.onConfirm = mockOnConfirm;
      spectator.component.control.setValue('invalid-value');
      spectator.detectChanges();

      await spectator.component.onConfirm();

      expect(mockOnConfirm).toHaveBeenCalledWith('invalid-value');
      expect(mockDialogRef.close).not.toHaveBeenCalled();
      expect(spectator.component.isLoading()).toBe(false);
      expect(spectator.component.asyncError()).toBe(errorMessage);
    });

    it('should display asyncError in template', async () => {
      const errorMessage = 'Card does not exist';
      const mockOnConfirm = jest
        .fn()
        .mockRejectedValue(new Error(errorMessage));
      mockData.onConfirm = mockOnConfirm;
      spectator.component.control.setValue('ERROR');
      spectator.detectChanges();

      await spectator.component.onConfirm();
      spectator.detectChanges();

      const errorElement = spectator.query('ui-text-field-errors span');
      expect(errorElement).toHaveText(errorMessage);
    });

    it('should use default error message when error has no message', async () => {
      const mockOnConfirm = jest.fn().mockRejectedValue({});
      mockData.onConfirm = mockOnConfirm;
      spectator.component.control.setValue('test');

      await spectator.component.onConfirm();

      expect(spectator.component.asyncError()).toBe(
        'Ein Fehler ist aufgetreten',
      );
    });
  });

  describe('asyncError clearing on input change', () => {
    it('should clear asyncError when user starts typing', async () => {
      // Set up component with error
      const mockOnConfirm = jest
        .fn()
        .mockRejectedValue(new Error('Initial error'));
      mockData.onConfirm = mockOnConfirm;
      spectator.component.control.setValue('initial');
      await spectator.component.onConfirm();

      expect(spectator.component.asyncError()).toBe('Initial error');

      // User types new value
      const input = spectator.query('input') as HTMLInputElement;
      spectator.typeInElement('new-value', input);
      spectator.detectChanges();

      // Error should be cleared
      expect(spectator.component.asyncError()).toBeNull();
    });

    it('should clear asyncError when control value changes programmatically', async () => {
      // Set up component with error
      spectator.component.asyncError.set('Some error');
      spectator.detectChanges();

      expect(spectator.component.asyncError()).toBe('Some error');

      // Change value programmatically
      spectator.component.control.setValue('new-value');
      spectator.detectChanges();

      // Error should be cleared
      expect(spectator.component.asyncError()).toBeNull();
    });
  });

  describe('button states', () => {
    it('should disable save button when control is invalid', () => {
      // Add a required validator to make control invalid
      spectator.component.control.addValidators((control) =>
        control.value ? null : { required: true },
      );
      spectator.component.control.setValue('');
      spectator.component.control.markAsTouched();
      spectator.component.control.updateValueAndValidity();
      spectator.detectChanges();

      const saveButton = spectator.query(
        '[data-which="save"]',
      ) as HTMLButtonElement;
      expect(saveButton.disabled).toBe(true);
    });

    it('should disable both buttons when isLoading is true', () => {
      spectator.component.isLoading.set(true);
      spectator.detectChanges();

      const closeButton = spectator.query(
        '[data-which="close"]',
      ) as HTMLButtonElement;
      const saveButton = spectator.query(
        '[data-which="save"]',
      ) as HTMLButtonElement;

      expect(closeButton.disabled).toBe(true);
      expect(saveButton.disabled).toBe(true);
    });

    it('should set pending state on save button when isLoading', () => {
      spectator.component.isLoading.set(true);
      spectator.detectChanges();

      const saveButton = spectator.query('[data-which="save"]');
      const debugElement = spectator.debugElement.query(
        (el) => el.nativeElement === saveButton,
      );

      // The [pending] input should be bound to isLoading()
      expect(debugElement?.componentInstance.pending()).toBe(true);
    });
  });
});
