import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QuantityControlComponent } from './quantity-control.component';
import { FormControl } from '@angular/forms';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { LiveAnnouncer } from '@angular/cdk/a11y';
import { TooltipDirective } from '@isa/ui/tooltip';

describe('QuantityControlComponent', () => {
  let component: QuantityControlComponent;
  let fixture: ComponentFixture<QuantityControlComponent>;
  let liveAnnouncerMock: { announce: ReturnType<typeof vi.fn> };
  let tooltipMock: {
    show: ReturnType<typeof vi.fn>;
    hide: ReturnType<typeof vi.fn>;
  };

  beforeEach(async () => {
    liveAnnouncerMock = {
      announce: vi.fn(),
    };

    tooltipMock = {
      show: vi.fn(),
      hide: vi.fn(),
    };

    await TestBed.configureTestingModule({
      imports: [QuantityControlComponent],
      providers: [
        { provide: LiveAnnouncer, useValue: liveAnnouncerMock },
        { provide: TooltipDirective, useValue: tooltipMock },
      ],
    })
      .overrideComponent(QuantityControlComponent, {
        remove: { imports: [TooltipDirective] },
      })
      .compileComponents();

    fixture = TestBed.createComponent(QuantityControlComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

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

    it('should have default value of 0', () => {
      expect(component.value()).toBe(0);
    });

    it('should not be disabled by default', () => {
      expect(component.disabled()).toBe(false);
    });

    it('should have default min of 0', () => {
      expect(component.min()).toBe(0);
    });

    it('should have default max of undefined', () => {
      expect(component.max()).toBeUndefined();
    });

    it('should have default presetLimit of 10', () => {
      expect(component.presetLimit()).toBe(10);
    });
  });

  describe('Generated Options', () => {
    it('should generate options from min to presetLimit with Edit', () => {
      const options = component.generatedOptions();

      expect(options).toHaveLength(11); // 0-9 + Edit
      expect(options[0]).toEqual({ value: 0, label: '0' });
      expect(options[9]).toEqual({ value: 9, label: '9' });
      expect(options[10]).toEqual({ value: 'edit', label: 'Edit' });
    });

    it('should generate options starting from 0 when min is 0', () => {
      fixture.componentRef.setInput('min', 0);
      fixture.detectChanges();

      const options = component.generatedOptions();

      expect(options[0]).toEqual({ value: 0, label: '0' });
      expect(options[9]).toEqual({ value: 9, label: '9' });
    });

    it('should not show Edit option when max is less than or equal to presetLimit', () => {
      fixture.componentRef.setInput('min', 1);
      fixture.componentRef.setInput('max', 5);
      fixture.componentRef.setInput('presetLimit', 10);
      fixture.detectChanges();

      const options = component.generatedOptions();

      expect(options).toHaveLength(5); // 1-5 (capped at max), no Edit
      expect(options[0]).toEqual({ value: 1, label: '1' });
      expect(options[4]).toEqual({ value: 5, label: '5' });
      expect(options[options.length - 1].value).not.toBe('edit');
    });

    it('should show Edit option when max exceeds presetLimit', () => {
      fixture.componentRef.setInput('max', 50);
      fixture.componentRef.setInput('presetLimit', 10);
      fixture.detectChanges();

      const options = component.generatedOptions();

      expect(options).toHaveLength(11); // 1-10 + Edit
      expect(options[options.length - 1]).toEqual({
        value: 'edit',
        label: 'Edit',
      });
    });

    it('should generate correct range with custom min and presetLimit', () => {
      fixture.componentRef.setInput('min', 5);
      fixture.componentRef.setInput('presetLimit', 3);
      fixture.detectChanges();

      const options = component.generatedOptions();

      expect(options[0]).toEqual({ value: 5, label: '5' });
      expect(options[1]).toEqual({ value: 6, label: '6' });
      expect(options[2]).toEqual({ value: 7, label: '7' });
    });
  });

  describe('Value Selection', () => {
    it('should update value when selectValue is called', () => {
      component.selectValue(5);

      expect(component.value()).toBe(5);
    });

    it('should call onChange callback when value changes', () => {
      const onChangeSpy = vi.fn();
      component.registerOnChange(onChangeSpy);

      component.selectValue(3);

      expect(onChangeSpy).toHaveBeenCalledWith(3);
    });

    it('should call onTouched callback when value changes', () => {
      const onTouchedSpy = vi.fn();
      component.registerOnTouched(onTouchedSpy);

      component.selectValue(7);

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

    it('should close dropdown after selecting value', () => {
      component.isOpen.set(true);

      component.selectValue(4);

      expect(component.isOpen()).toBe(false);
    });

    it('should announce value change to screen readers', () => {
      component.selectValue(8);

      expect(liveAnnouncerMock.announce).toHaveBeenCalledWith(
        'Quantity changed to 8',
        'polite',
      );
    });
  });

  describe('Dropdown State', () => {
    it('should open dropdown when open() is called', () => {
      component.open();

      expect(component.isOpen()).toBe(true);
    });

    it('should not open dropdown when disabled', () => {
      component.disabled.set(true);

      component.open();

      expect(component.isOpen()).toBe(false);
    });

    it('should close dropdown when close() is called', () => {
      component.isOpen.set(true);

      component.close();

      expect(component.isOpen()).toBe(false);
    });

    it('should toggle dropdown state', () => {
      component.toggleDropdown();
      expect(component.isOpen()).toBe(true);

      component.toggleDropdown();
      expect(component.isOpen()).toBe(false);
    });

    it('should not toggle when disabled', () => {
      component.disabled.set(true);

      component.toggleDropdown();

      expect(component.isOpen()).toBe(false);
    });
  });

  describe('Edit Mode', () => {
    it('should enter edit mode when enterEditMode is called', () => {
      component.enterEditMode();

      expect(component.isEditMode()).toBe(true);
      expect(component.isOpen()).toBe(false);
    });

    it('should announce edit mode activation', () => {
      component.enterEditMode();

      expect(liveAnnouncerMock.announce).toHaveBeenCalledWith(
        'Edit mode activated. Type a quantity and press Enter to confirm or Escape to cancel.',
        'polite',
      );
    });

    it('should exit edit mode with valid value', () => {
      component.enterEditMode();

      component.exitEditMode('15');

      expect(component.isEditMode()).toBe(false);
      expect(component.value()).toBe(15);
    });

    it('should clamp to minimum value when input is too low', () => {
      fixture.componentRef.setInput('min', 1);
      fixture.detectChanges();
      component.enterEditMode();

      component.exitEditMode('0');

      expect(component.isEditMode()).toBe(false);
      expect(component.value()).toBe(1); // Clamped to min
      expect(liveAnnouncerMock.announce).toHaveBeenCalledWith(
        'Adjusted to minimum 1',
        'polite',
      );
    });

    it('should clamp to maximum value when input is too high', () => {
      fixture.componentRef.setInput('max', 50);
      fixture.detectChanges();
      component.enterEditMode();

      component.exitEditMode('100');

      expect(component.isEditMode()).toBe(false);
      expect(component.value()).toBe(50); // Clamped to max
      expect(liveAnnouncerMock.announce).toHaveBeenCalledWith(
        'Adjusted to maximum 50',
        'polite',
      );
    });

    it('should reject invalid (NaN) input in edit mode', () => {
      const originalValue = component.value();
      component.enterEditMode();

      component.exitEditMode('abc');

      expect(component.isEditMode()).toBe(false);
      expect(component.value()).toBe(originalValue); // Value unchanged
      expect(liveAnnouncerMock.announce).toHaveBeenCalledWith(
        'Invalid input. Please enter a valid number.',
        'assertive',
      );
    });

    it('should cancel edit mode without changing value', () => {
      const originalValue = component.value();
      component.enterEditMode();

      component.cancelEditMode();

      expect(component.isEditMode()).toBe(false);
      expect(component.value()).toBe(originalValue);
    });

    it('should announce cancel', () => {
      component.enterEditMode();

      component.cancelEditMode();

      expect(liveAnnouncerMock.announce).toHaveBeenCalledWith(
        'Edit mode cancelled',
        'polite',
      );
    });

    it('should prevent double-exit when called multiple times', () => {
      component.enterEditMode();
      const onChangeSpy = vi.fn();
      component.registerOnChange(onChangeSpy);

      component.exitEditMode('5');
      component.exitEditMode('10'); // Should be ignored

      expect(component.value()).toBe(5);
      expect(onChangeSpy).toHaveBeenCalledTimes(1);
    });
  });

  describe('ControlValueAccessor', () => {
    it('should write value', () => {
      component.writeValue(7);

      expect(component.value()).toBe(7);
    });

    it('should reset to min when writing invalid value', () => {
      fixture.componentRef.setInput('min', 1);
      fixture.detectChanges();

      component.writeValue(-5);

      expect(component.value()).toBe(1);
    });

    it('should reset to min when writing NaN', () => {
      fixture.componentRef.setInput('min', 1);
      fixture.detectChanges();

      component.writeValue('invalid');

      expect(component.value()).toBe(1);
    });

    it('should set disabled state', () => {
      component.setDisabledState(true);

      expect(component.disabled()).toBe(true);
    });

    it('should work with FormControl', () => {
      const formControl = new FormControl(5);

      component.writeValue(formControl.value);
      component.registerOnChange((value) => formControl.setValue(value));

      component.selectValue(10);

      expect(component.value()).toBe(10);
    });
  });

  describe('Keyboard Navigation', () => {
    it('should open dropdown on Enter key when closed', () => {
      const event = new KeyboardEvent('keydown', { key: 'Enter' });
      const preventDefaultSpy = vi.spyOn(event, 'preventDefault');

      component.onKeydown(event);

      expect(component.isOpen()).toBe(true);
      expect(preventDefaultSpy).toHaveBeenCalled();
    });

    it('should open dropdown on Space key when closed', () => {
      const event = new KeyboardEvent('keydown', { key: ' ' });
      const preventDefaultSpy = vi.spyOn(event, 'preventDefault');

      component.onKeydown(event);

      expect(component.isOpen()).toBe(true);
      expect(preventDefaultSpy).toHaveBeenCalled();
    });

    it('should close dropdown on Escape key when open', () => {
      component.isOpen.set(true);
      const event = new KeyboardEvent('keydown', { key: 'Escape' });
      const preventDefaultSpy = vi.spyOn(event, 'preventDefault');

      component.onKeydown(event);

      expect(component.isOpen()).toBe(false);
      expect(preventDefaultSpy).toHaveBeenCalled();
    });

    it('should not handle keys when disabled', () => {
      component.disabled.set(true);
      const event = new KeyboardEvent('keydown', { key: 'Enter' });

      component.onKeydown(event);

      expect(component.isOpen()).toBe(false);
    });

    it('should not handle keys when in edit mode', () => {
      component.isEditMode.set(true);
      const event = new KeyboardEvent('keydown', { key: 'Enter' });

      component.onKeydown(event);

      expect(component.isOpen()).toBe(false);
    });
  });

  describe('ARIA Attributes', () => {
    it('should have unique listbox ID', () => {
      expect(component.listboxId).toContain('quantity-control-listbox-');
    });

    it('should compute activeDescendantId when dropdown is open', () => {
      component.isOpen.set(true);
      fixture.detectChanges();

      // Without active item, should be null
      expect(component.activeDescendantId()).toBeNull();
    });

    it('should compute activeDescendantId as null when dropdown is closed', () => {
      component.isOpen.set(false);

      expect(component.activeDescendantId()).toBeNull();
    });

    it('should compute chevron icon based on dropdown state', () => {
      expect(component.chevronIcon()).toBe('isaActionChevronDown');

      component.isOpen.set(true);
      expect(component.chevronIcon()).toBe('isaActionChevronUp');
    });
  });

  describe('Custom Inputs', () => {
    it('should accept custom min value', () => {
      fixture.componentRef.setInput('min', 0);
      fixture.detectChanges();

      expect(component.min()).toBe(0);
    });

    it('should accept custom max value', () => {
      fixture.componentRef.setInput('max', 100);
      fixture.detectChanges();

      expect(component.max()).toBe(100);
    });

    it('should accept custom presetLimit', () => {
      fixture.componentRef.setInput('presetLimit', 20);
      fixture.detectChanges();

      expect(component.presetLimit()).toBe(20);
    });

    it('should accept custom ariaLabel', () => {
      fixture.componentRef.setInput('ariaLabel', 'Select quantity');
      fixture.detectChanges();

      expect(component.ariaLabel()).toBe('Select quantity');
    });
  });

  describe('Tooltip Validation', () => {
    beforeEach(() => {
      // Setup tooltip mock on component
      component.tooltipDir = vi.fn(() => tooltipMock) as any;
    });

    it('should show tooltip when input is less than min', () => {
      fixture.componentRef.setInput('min', 1);
      fixture.detectChanges();
      component.enterEditMode();

      component.onInputChange('0');

      expect(component.validationMessage()).toBe('Minimum ist 1');
      expect(tooltipMock.show).toHaveBeenCalled();
    });

    it('should show tooltip when input is greater than max', () => {
      fixture.componentRef.setInput('max', 50);
      fixture.detectChanges();
      component.enterEditMode();

      component.onInputChange('100');

      expect(component.validationMessage()).toBe('Maximum ist 50');
      expect(tooltipMock.show).toHaveBeenCalled();
    });

    it('should hide tooltip when valid input is entered', () => {
      fixture.componentRef.setInput('min', 1);
      fixture.componentRef.setInput('max', 50);
      fixture.detectChanges();
      component.enterEditMode();

      // First enter invalid input
      component.onInputChange('0');
      expect(tooltipMock.show).toHaveBeenCalled();

      // Then enter valid input
      tooltipMock.show.mockClear();
      component.onInputChange('25');

      expect(component.validationMessage()).toBe('');
      expect(tooltipMock.hide).toHaveBeenCalled();
    });

    it('should not show tooltip for valid input on initial entry', () => {
      fixture.componentRef.setInput('min', 1);
      fixture.componentRef.setInput('max', 50);
      fixture.detectChanges();
      component.enterEditMode();

      component.onInputChange('25');

      expect(component.validationMessage()).toBe('');
      expect(tooltipMock.show).not.toHaveBeenCalled();
    });

    it('should handle empty input without showing tooltip', () => {
      fixture.componentRef.setInput('min', 1);
      fixture.detectChanges();
      component.enterEditMode();

      component.onInputChange('');

      expect(component.validationMessage()).toBe('');
      expect(tooltipMock.show).not.toHaveBeenCalled();
    });

    it('should handle NaN input without showing tooltip', () => {
      fixture.componentRef.setInput('min', 1);
      fixture.detectChanges();
      component.enterEditMode();

      component.onInputChange('abc');

      expect(component.validationMessage()).toBe('');
      expect(tooltipMock.show).not.toHaveBeenCalled();
    });
  });
});
