import { Component, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { RatingComponent } from './rating.component';

@Component({
  standalone: false,
  template: `
    <usr-rating
      [starCount]="starCount"
      [ratingValue]="ratingValue"
      [size]="size"
      [activeColor]="activeColor"
      [inactiveColor]="inactiveColor"
      [isReadOnly]="isReadOnly"
      [isDisabled]="isDisabled"
      [visible]="visible"
      (ratingValueChange)="onRatingValueChange($event)"
    ></usr-rating>
  `,
})
class TestHostComponent {
  public starCount = 5;
  public ratingValue = 0;
  public size: 'small' | 'medium' | 'large' = 'medium';
  public activeColor = '#FFC107';
  public inactiveColor = '#D1D5DB';
  public isReadOnly = false;
  public isDisabled = false;
  public visible = true;

  public readonly ratingValueChangeSpy = jest.fn();

  @ViewChild(RatingComponent)
  public readonly component!: RatingComponent;

  public onRatingValueChange(value: number): void {
    this.ratingValueChangeSpy(value);
  }
}

describe('RatingComponent', () => {
  let fixture: ComponentFixture<TestHostComponent>;
  let host: TestHostComponent;

  function getNativeElement(): HTMLElement {
    const result = fixture.nativeElement.querySelector('usr-rating') as HTMLElement | null;
    if (!result) {
      throw new Error('usr-rating element not found');
    }
    return result;
  }

  function getShadowRoot(): ShadowRoot {
    const shadowRoot = getNativeElement().shadowRoot;
    if (!shadowRoot) {
      throw new Error('usr-rating shadowRoot not found');
    }
    return shadowRoot;
  }

  function getStarButtons(): HTMLButtonElement[] {
    return Array.from(getShadowRoot().querySelectorAll<HTMLButtonElement>('[data-testid="usr-rating-star"]'));
  }

  afterEach(() => {
    jest.restoreAllMocks();
  });

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [RatingComponent, TestHostComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(TestHostComponent);
    host = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(host.component).toBeDefined();
  });

  describe('rendering', () => {
    it('should render in shadow DOM', () => {
      expect(getNativeElement().shadowRoot).not.toBeNull();
    });

    it('should render starCount number of star buttons', () => {
      expect(getStarButtons().length).toBe(5);
    });

    it('should render nothing when visible is false', () => {
      host.visible = false;
      fixture.detectChanges();

      expect(getShadowRoot().querySelector('[data-testid="usr-rating"]')).toBeNull();
    });

    it('should default to zero selected stars', () => {
      const buttons = getStarButtons();
      expect(buttons.every((button) => button.getAttribute('aria-checked') === 'false')).toBe(true);
    });
  });

  describe('star click', () => {
    it('should set ratingValue and emit ratingValueChange when a star is clicked', () => {
      const buttons = getStarButtons();
      buttons[2].click();
      fixture.detectChanges();

      expect(host.component.ratingValue).toBe(3);
      expect(host.ratingValueChangeSpy).toHaveBeenCalledWith(3);
    });

    it('should mark the clicked star as checked and fill preceding stars', () => {
      const buttons = getStarButtons();
      buttons[2].click();
      fixture.detectChanges();

      const refreshed = getStarButtons();
      expect(refreshed[2].getAttribute('aria-checked')).toBe('true');
      expect(refreshed[0].getAttribute('aria-checked')).toBe('false');
      expect(refreshed[0].textContent).toContain('★');
    });

    it('should not change value when read-only', () => {
      host.isReadOnly = true;
      fixture.detectChanges();

      expect(getShadowRoot().querySelector('[data-testid="usr-rating-group"]')).toBeNull();
      expect(getShadowRoot().querySelector('[data-testid="usr-rating-display"]')).not.toBeNull();
    });

    it('should render disabled buttons and ignore clicks when disabled', () => {
      host.isDisabled = true;
      fixture.detectChanges();

      const buttons = getStarButtons();
      expect(buttons[0].disabled).toBe(true);

      buttons[0].click();
      fixture.detectChanges();

      expect(host.ratingValueChangeSpy).not.toHaveBeenCalled();
      expect(host.component.ratingValue).toBe(0);
    });
  });

  describe('ngOnChanges clamping', () => {
    it('should clamp ratingValue below 0 up to 0', () => {
      host.ratingValue = -3;
      fixture.detectChanges();

      expect(host.component.ratingValue).toBe(0);
    });

    it('should clamp ratingValue above starCount down to starCount', () => {
      host.ratingValue = 20;
      fixture.detectChanges();

      expect(host.component.ratingValue).toBe(5);
    });

    it('should re-clamp an existing rating value when starCount shrinks', () => {
      host.ratingValue = 5;
      fixture.detectChanges();

      host.starCount = 3;
      fixture.detectChanges();

      expect(host.component.ratingValue).toBe(3);
      expect(host.ratingValueChangeSpy).toHaveBeenCalledWith(3);
    });
  });

  describe('keyboard navigation', () => {
    it('should increase rating on ArrowRight and ArrowUp', () => {
      host.ratingValue = 2;
      fixture.detectChanges();

      const group = getShadowRoot().querySelector('[data-testid="usr-rating-group"]') as HTMLElement;
      group.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
      fixture.detectChanges();
      expect(host.component.ratingValue).toBe(3);

      group.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
      fixture.detectChanges();
      expect(host.component.ratingValue).toBe(4);
    });

    it('should decrease rating on ArrowLeft and ArrowDown', () => {
      host.ratingValue = 4;
      fixture.detectChanges();

      const group = getShadowRoot().querySelector('[data-testid="usr-rating-group"]') as HTMLElement;
      group.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }));
      fixture.detectChanges();
      expect(host.component.ratingValue).toBe(3);

      group.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
      fixture.detectChanges();
      expect(host.component.ratingValue).toBe(2);
    });

    it('should set rating to 0 on Home and starCount on End', () => {
      host.ratingValue = 3;
      fixture.detectChanges();

      const group = getShadowRoot().querySelector('[data-testid="usr-rating-group"]') as HTMLElement;
      group.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true }));
      fixture.detectChanges();
      expect(host.component.ratingValue).toBe(0);

      group.dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true }));
      fixture.detectChanges();
      expect(host.component.ratingValue).toBe(5);
    });

    it('should clamp at boundaries and not emit when already at the limit', () => {
      host.ratingValue = 5;
      fixture.detectChanges();
      host.ratingValueChangeSpy.mockClear();

      const group = getShadowRoot().querySelector('[data-testid="usr-rating-group"]') as HTMLElement;
      group.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
      fixture.detectChanges();

      expect(host.component.ratingValue).toBe(5);
      expect(host.ratingValueChangeSpy).not.toHaveBeenCalled();
    });

    it('should ignore keyboard input when disabled', () => {
      host.isDisabled = true;
      host.ratingValue = 2;
      fixture.detectChanges();

      const group = getShadowRoot().querySelector('[data-testid="usr-rating-group"]') as HTMLElement;
      group.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
      fixture.detectChanges();

      expect(host.component.ratingValue).toBe(2);
    });
  });

  describe('accessibility', () => {
    it('should have an accessible name for the radiogroup', () => {
      const group = getShadowRoot().querySelector('[data-testid="usr-rating-group"]');
      expect(group?.getAttribute('aria-label')).toBeTruthy();
    });

    it('should have a distinct accessible name for each star', () => {
      const buttons = getStarButtons();
      expect(buttons[0].getAttribute('aria-label')).toBe('1 star');
      expect(buttons[1].getAttribute('aria-label')).toBe('2 stars');
    });

    it('should expose a live region reflecting the current value', () => {
      host.ratingValue = 4;
      fixture.detectChanges();

      const live = getShadowRoot().querySelector('[data-testid="usr-rating-live"]');
      expect(live?.getAttribute('aria-live')).toBe('polite');
      expect(live?.textContent).toContain('4');
    });

    it('should use semantic button elements, not divs, for each star', () => {
      const buttons = getStarButtons();
      expect(buttons.every((button) => button.tagName === 'BUTTON')).toBe(true);
    });
  });

  describe('size and color inputs', () => {
    it('should apply the size modifier class', () => {
      host.size = 'large';
      fixture.detectChanges();

      const root = getShadowRoot().querySelector('[data-testid="usr-rating"]');
      expect(root?.classList.contains('usr-rating--large')).toBe(true);
    });

    it('should bind active/inactive colors as CSS custom properties', () => {
      host.activeColor = '#123456';
      host.inactiveColor = '#abcdef';
      fixture.detectChanges();

      const root = getShadowRoot().querySelector('[data-testid="usr-rating"]') as HTMLElement;
      expect(root.style.getPropertyValue('--star-active-color')).toBe('#123456');
      expect(root.style.getPropertyValue('--star-inactive-color')).toBe('#abcdef');
    });
  });

  describe('edge cases', () => {
    it('should support starCount at the minimum boundary (1)', () => {
      host.starCount = 1;
      host.ratingValue = 1;
      fixture.detectChanges();

      expect(getStarButtons().length).toBe(1);
      expect(host.component.ratingValue).toBe(1);
    });

    it('should support starCount at the maximum boundary (10)', () => {
      host.starCount = 10;
      host.ratingValue = 10;
      fixture.detectChanges();

      expect(getStarButtons().length).toBe(10);
      expect(host.component.ratingValue).toBe(10);
    });
  });
});
