import { Component } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { By } from '@angular/platform-browser';
import { DropdownButtonComponent } from './dropdown.component';
import { DropdownOptionComponent } from './dropdown-option.component';
import { DropdownAppearance } from './dropdown.types';

/**
 * Test host component with basic dropdown
 */
@Component({
  selector: 'ui-test-host',
  standalone: true,
  imports: [
    DropdownButtonComponent,
    DropdownOptionComponent,
    FormsModule,
    ReactiveFormsModule,
  ],
  template: `
    <ui-dropdown
      [appearance]="appearance"
      [label]="label"
      [disabled]="disabled"
      [showSelectedValue]="showSelectedValue"
      [(ngModel)]="selectedValue"
    >
      <ui-dropdown-option [value]="'option1'">Option 1</ui-dropdown-option>
      <ui-dropdown-option [value]="'option2'">Option 2</ui-dropdown-option>
      <ui-dropdown-option [value]="'option3'" [disabled]="true">
        Disabled Option
      </ui-dropdown-option>
    </ui-dropdown>
  `,
})
class TestHostComponent {
  appearance = DropdownAppearance.AccentOutline;
  label = 'Select an option';
  disabled = false;
  showSelectedValue = true;
  selectedValue: string | undefined;
}

describe('DropdownButtonComponent', () => {
  let spectator: Spectator<TestHostComponent>;

  const createComponent = createComponentFactory({
    component: TestHostComponent,
  });

  beforeEach(() => {
    spectator = createComponent();
  });

  afterEach(() => {
    // Clean up any open overlays
    document.querySelectorAll('.cdk-overlay-container').forEach((el) => {
      el.innerHTML = '';
    });
  });

  const getDropdown = () =>
    spectator.debugElement
      .query(By.directive(DropdownButtonComponent))
      .componentInstance as DropdownButtonComponent<string>;

  const getDropdownElement = () =>
    spectator.query('ui-dropdown') as HTMLElement;

  const openDropdown = () => {
    getDropdown().open();
    spectator.detectChanges();
  };

  const getOptionElements = () =>
    Array.from(
      document.querySelectorAll('ui-dropdown-option'),
    ) as HTMLElement[];

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

  describe('Open/Close Behavior', () => {
    it('should open dropdown on click', () => {
      // Act
      spectator.click(getDropdownElement());
      spectator.detectChanges();

      // Assert
      expect(getDropdown().isOpen()).toBe(true);
      expect(getOptionElements().length).toBe(3);
    });

    it('should close dropdown on second click', () => {
      // Arrange
      openDropdown();
      expect(getDropdown().isOpen()).toBe(true);

      // Act
      spectator.click(getDropdownElement());
      spectator.detectChanges();

      // Assert
      expect(getDropdown().isOpen()).toBe(false);
    });

    it('should close dropdown on Escape key', () => {
      // Arrange
      openDropdown();

      // Act
      spectator.dispatchKeyboardEvent(getDropdownElement(), 'keydown', 'Escape');
      spectator.detectChanges();

      // Assert
      expect(getDropdown().isOpen()).toBe(false);
    });

    it('should close dropdown when option is selected', () => {
      // Arrange
      openDropdown();

      // Act
      getOptionElements()[0].click();
      spectator.detectChanges();

      // Assert
      expect(getDropdown().isOpen()).toBe(false);
    });

    it('should NOT open when disabled', () => {
      // Arrange
      spectator.component.disabled = true;
      spectator.detectChanges();

      // Act
      spectator.click(getDropdownElement());
      spectator.detectChanges();

      // Assert
      expect(getDropdown().isOpen()).toBe(false);
    });
  });

  describe('ARIA Attributes', () => {
    it('should have role="combobox"', () => {
      expect(getDropdownElement().getAttribute('role')).toBe('combobox');
    });

    it('should have aria-haspopup="listbox"', () => {
      expect(getDropdownElement().getAttribute('aria-haspopup')).toBe(
        'listbox',
      );
    });

    // NOTE: aria-expanded is incorrectly bound in the original code as a literal string
    // This will be fixed during refactoring
    it('should have aria-expanded attribute', () => {
      // Currently bound incorrectly as literal string - see host binding
      // The attribute should dynamically reflect isOpen() state
      expect(getDropdownElement().hasAttribute('aria-expanded')).toBe(true);
    });

    it('should have tabindex="0" by default', () => {
      expect(getDropdownElement().getAttribute('tabindex')).toBe('0');
    });

    it('should have tabindex="-1" when disabled', () => {
      // Arrange
      spectator.component.disabled = true;
      spectator.detectChanges();

      // Assert
      expect(getDropdownElement().getAttribute('tabindex')).toBe('-1');
    });
  });

  describe('CSS Classes', () => {
    it('should have base class ui-dropdown', () => {
      expect(getDropdownElement()).toHaveClass('ui-dropdown');
    });

    it('should have accent-outline appearance by default', () => {
      expect(getDropdownElement()).toHaveClass('ui-dropdown__accent-outline');
    });

    it('should apply grey appearance', () => {
      // Arrange
      (spectator.component as { appearance: string }).appearance =
        DropdownAppearance.Grey;
      spectator.detectChanges();

      // Assert
      expect(getDropdownElement()).toHaveClass('ui-dropdown__grey');
    });

    it('should add "open" class when open', () => {
      // Arrange
      openDropdown();

      // Assert
      expect(getDropdownElement()).toHaveClass('open');
    });

    it('should add "disabled" class when disabled', () => {
      // Arrange
      spectator.component.disabled = true;
      spectator.detectChanges();

      // Assert
      expect(getDropdownElement()).toHaveClass('disabled');
    });

    it('should add "has-value" class when value is set', fakeAsync(() => {
      // Arrange - set value directly on dropdown component since ngModel needs time
      getDropdown().value.set('option1');
      spectator.detectChanges();
      tick();

      // Assert
      expect(getDropdownElement()).toHaveClass('has-value');
    }));
  });

  describe('Label Display', () => {
    it('should display label when no value selected', () => {
      const labelElement = spectator.query('.ui-dropdown__text');
      expect(labelElement?.textContent?.trim()).toBe('Select an option');
    });

    it('should display selected option label when showSelectedValue is true and no label set', fakeAsync(() => {
      // viewLabel logic: label() ?? selectedOption.getLabel()
      // So we need to clear the label to see the selected option's label
      spectator.component.label = undefined as unknown as string;
      spectator.detectChanges();

      // Arrange - select an option
      openDropdown();
      getOptionElements()[0].click();
      spectator.detectChanges();
      tick();

      // Assert - with no label set, viewLabel returns selectedOption.getLabel()
      const labelElement = spectator.query('.ui-dropdown__text');
      expect(labelElement?.textContent?.trim()).toBe('Option 1');
    }));

    it('should display original label when showSelectedValue is false', fakeAsync(() => {
      // Arrange
      spectator.component.showSelectedValue = false;
      spectator.detectChanges();
      openDropdown();

      // Act
      getOptionElements()[0].click();
      spectator.detectChanges();
      tick();

      // Assert
      const labelElement = spectator.query('.ui-dropdown__text');
      expect(labelElement?.textContent?.trim()).toBe('Select an option');
    }));
  });

  describe('ControlValueAccessor', () => {
    it('should implement writeValue correctly', () => {
      // Act
      getDropdown().writeValue('option2');
      spectator.detectChanges();

      // Assert
      expect(getDropdown().value()).toBe('option2');
    });

    it('should call onChange when value changes', () => {
      // Arrange
      const onChangeSpy = jest.fn();
      getDropdown().registerOnChange(onChangeSpy);
      openDropdown();

      // Act
      getOptionElements()[0].click();
      spectator.detectChanges();

      // Assert
      expect(onChangeSpy).toHaveBeenCalledWith('option1');
    });

    it('should call onTouched when value changes', () => {
      // Arrange
      const onTouchedSpy = jest.fn();
      getDropdown().registerOnTouched(onTouchedSpy);
      openDropdown();

      // Act
      getOptionElements()[0].click();
      spectator.detectChanges();

      // Assert
      expect(onTouchedSpy).toHaveBeenCalled();
    });

    it('should set disabled state', () => {
      // Act
      getDropdown().setDisabledState?.(true);
      spectator.detectChanges();

      // Assert
      expect(getDropdown().disabled()).toBe(true);
      expect(getDropdownElement()).toHaveClass('disabled');
    });

    it('should update host component value via ngModel', fakeAsync(() => {
      // Arrange
      openDropdown();

      // Act
      getOptionElements()[1].click();
      spectator.detectChanges();
      tick();

      // Assert
      expect(spectator.component.selectedValue).toBe('option2');
    }));
  });

  describe('Keyboard Navigation', () => {
    it('should navigate down with ArrowDown', () => {
      // Arrange
      openDropdown();
      const dropdown = getDropdown();

      // Act
      spectator.dispatchKeyboardEvent(
        getDropdownElement(),
        'keydown',
        'ArrowDown',
      );
      spectator.detectChanges();

      // Assert - keyManager should have active item
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      expect((dropdown as any)['keyManager']?.activeItem).toBeTruthy();
    });

    it('should skip disabled options', () => {
      // Arrange
      openDropdown();
      const dropdown = getDropdown();

      // Navigate to second option
      spectator.dispatchKeyboardEvent(
        getDropdownElement(),
        'keydown',
        'ArrowDown',
      );
      spectator.dispatchKeyboardEvent(
        getDropdownElement(),
        'keydown',
        'ArrowDown',
      );
      spectator.detectChanges();

      // Should skip the disabled third option and wrap to first
      // (keyManager has withWrap() enabled)
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      expect((dropdown as any)['keyManager']?.activeItem?.disabled).toBeFalsy();
    });

    it('should select option on Enter', () => {
      // Arrange
      openDropdown();
      const dropdown = getDropdown();

      // Navigate to first option
      spectator.dispatchKeyboardEvent(
        getDropdownElement(),
        'keydown',
        'ArrowDown',
      );
      spectator.detectChanges();

      // Act - press Enter
      spectator.dispatchKeyboardEvent(
        getDropdownElement(),
        'keydown',
        'Enter',
      );
      spectator.detectChanges();

      // Assert
      expect(dropdown.isOpen()).toBe(false);
    });

    it('should open dropdown on Space key when closed', () => {
      // Arrange
      const dropdown = getDropdown();
      expect(dropdown.isOpen()).toBe(false);

      // Act
      spectator.dispatchKeyboardEvent(getDropdownElement(), 'keydown', ' ');
      spectator.detectChanges();

      // Assert
      expect(dropdown.isOpen()).toBe(true);
    });

    it('should close dropdown on Space key when open', () => {
      // Arrange
      openDropdown();
      const dropdown = getDropdown();
      expect(dropdown.isOpen()).toBe(true);

      // Act
      spectator.dispatchKeyboardEvent(getDropdownElement(), 'keydown', ' ');
      spectator.detectChanges();

      // Assert
      expect(dropdown.isOpen()).toBe(false);
    });

    it('should select active option on Space key when open', () => {
      // Arrange
      openDropdown();
      const dropdown = getDropdown();

      // Navigate to first option
      spectator.dispatchKeyboardEvent(
        getDropdownElement(),
        'keydown',
        'ArrowDown',
      );
      spectator.detectChanges();

      // Act - press Space
      spectator.dispatchKeyboardEvent(getDropdownElement(), 'keydown', ' ');
      spectator.detectChanges();

      // Assert - dropdown should be closed and value selected
      expect(dropdown.isOpen()).toBe(false);
      expect(dropdown.value()).toBe('option1');
    });

    it('should prevent default scroll behavior on Space key', () => {
      // Arrange
      const event = new KeyboardEvent('keydown', { key: ' ' });
      const preventDefaultSpy = jest.spyOn(event, 'preventDefault');

      // Act
      getDropdown().onSpaceKey(event);

      // Assert
      expect(preventDefaultSpy).toHaveBeenCalled();
    });
  });

  describe('Icon Display', () => {
    it('should show chevron down when closed', () => {
      expect(getDropdown().isOpenIcon()).toBe('isaActionChevronDown');
    });

    it('should show chevron up when open', () => {
      // Arrange
      openDropdown();

      // Assert
      expect(getDropdown().isOpenIcon()).toBe('isaActionChevronUp');
    });
  });
});

/**
 * Test with FormControl
 */
@Component({
  selector: 'ui-test-host-form-control',
  standalone: true,
  imports: [
    DropdownButtonComponent,
    DropdownOptionComponent,
    ReactiveFormsModule,
  ],
  template: `
    <ui-dropdown [formControl]="control">
      <ui-dropdown-option [value]="1">One</ui-dropdown-option>
      <ui-dropdown-option [value]="2">Two</ui-dropdown-option>
    </ui-dropdown>
  `,
})
class TestHostFormControlComponent {
  control = new FormControl<number | null>(null);
}

describe('DropdownButtonComponent with FormControl', () => {
  let spectator: Spectator<TestHostFormControlComponent>;

  const createComponent = createComponentFactory({
    component: TestHostFormControlComponent,
  });

  beforeEach(() => {
    spectator = createComponent();
  });

  afterEach(() => {
    document.querySelectorAll('.cdk-overlay-container').forEach((el) => {
      el.innerHTML = '';
    });
  });

  const getDropdown = () =>
    spectator.debugElement
      .query(By.directive(DropdownButtonComponent))
      .componentInstance as DropdownButtonComponent<number>;

  it('should sync with FormControl value', () => {
    // Act
    spectator.component.control.setValue(2);
    spectator.detectChanges();

    // Assert
    expect(getDropdown().value()).toBe(2);
  });

  it('should update FormControl when option selected', () => {
    // Arrange
    getDropdown().open();
    spectator.detectChanges();
    const options = Array.from(
      document.querySelectorAll('ui-dropdown-option'),
    ) as HTMLElement[];

    // Act
    options[0].click();
    spectator.detectChanges();

    // Assert
    expect(spectator.component.control.value).toBe(1);
  });

  it('should disable dropdown when FormControl is disabled', () => {
    // Act
    spectator.component.control.disable();
    spectator.detectChanges();

    // Assert
    expect(getDropdown().disabled()).toBe(true);
  });
});
