import { mount } from '@vue/test-utils';
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { nextTick } from 'vue';
import MCarousel from './MCarousel.vue';
import MIconButton from '../iconbutton/MIconButton.vue';
import { ChevronLeft20, ChevronRight20 } from '@mozaic-ds/icons-vue';

/* eslint-disable @typescript-eslint/no-explicit-any */

class MockResizeObserver {
  callback: any;
  constructor(callback: any) {
    this.callback = callback;
  }
  observe = vi.fn();
  unobserve = vi.fn();
  disconnect = vi.fn();
}

describe('MCarousel component', () => {
  let originalResizeObserver: any;

  beforeAll(() => {
    originalResizeObserver = global.ResizeObserver;
    global.ResizeObserver = MockResizeObserver as any;

    Object.defineProperty(window.HTMLElement.prototype, 'scrollIntoView', {
      value: vi.fn(),
      writable: true,
    });

    // Mock getBoundingClientRect for visibility tests
    window.HTMLElement.prototype.getBoundingClientRect = vi.fn(() => ({
      left: 0,
      top: 0,
      right: 100,
      bottom: 100,
      width: 100,
      height: 100,
      x: 0,
      y: 0,
      toJSON: () => {},
    }));
  });

  afterAll(() => {
    global.ResizeObserver = originalResizeObserver;
  });

  const mockChildren = [
    '<div class="slide">Slide 1</div>',
    '<div class="slide">Slide 2</div>',
    '<div class="slide">Slide 3</div>',
  ];

  const mountCarousel = (options = {}) =>
    mount(MCarousel, {
      attachTo: document.body,
      slots: {
        default: mockChildren.join(''),
        header: '<h2 id="mc-carousel__title">Carousel Header</h2>',
      },
      ...options,
    });

  it('renders correctly with header and default slot', () => {
    const wrapper = mountCarousel();
    expect(wrapper.find('.mc-carousel__headings').text()).toContain(
      'Carousel Header',
    );
    expect(wrapper.findAll('.slide')).toHaveLength(3);
  });

  it('renders navigation buttons with correct aria labels', () => {
    const wrapper = mountCarousel({
      props: {
        previousButtonAriaLabel: 'Go back',
        nextButtonAriaLabel: 'Go forward',
      },
    });

    const buttons = wrapper.findAllComponents(MIconButton);
    expect(buttons).toHaveLength(2);
    expect(buttons[0].attributes('aria-label')).toBe('Go back');
    expect(buttons[1].attributes('aria-label')).toBe('Go forward');
  });

  it('renders default aria labels when not provided', () => {
    const wrapper = mountCarousel();
    const buttons = wrapper.findAllComponents(MIconButton);
    expect(buttons[0].attributes('aria-label')).toBe('previous');
    expect(buttons[1].attributes('aria-label')).toBe('next');
  });

  it('renders icon components inside navigation buttons', () => {
    const wrapper = mountCarousel();
    expect(wrapper.findComponent(ChevronLeft20).exists()).toBe(true);
    expect(wrapper.findComponent(ChevronRight20).exists()).toBe(true);
  });

  it('disables the previous button when on the first slide', async () => {
    const wrapper = mountCarousel();

    // Mock container at start position
    const contentContainer = wrapper.find('.mc-carousel__content');
    Object.defineProperty(contentContainer.element, 'scrollLeft', {
      value: 0,
      writable: true,
      configurable: true,
    });
    Object.defineProperty(contentContainer.element, 'scrollWidth', {
      value: 300,
      writable: true,
      configurable: true,
    });
    Object.defineProperty(contentContainer.element, 'clientWidth', {
      value: 100,
      writable: true,
      configurable: true,
    });

    // Trigger scroll to update state
    await contentContainer.trigger('scroll');
    await nextTick();

    const [prevButton] = wrapper.findAllComponents(MIconButton);
    expect(prevButton.props('disabled')).toBe(true);
  });

  it('enables next button when not on last slide', async () => {
    const wrapper = mountCarousel({
      slots: {
        default:
          '<div class="slide">Slide 1</div><div class="slide">Slide 2</div><div class="slide">Slide 3</div>',
        header: '<h2 id="mc-carousel__title">Carousel Header</h2>',
      },
    });

    // Mock container with scrollable content (on first slide)
    const contentContainer = wrapper.find('.mc-carousel__content');
    Object.defineProperty(contentContainer.element, 'scrollLeft', {
      value: 0,
      writable: true,
      configurable: true,
    });
    Object.defineProperty(contentContainer.element, 'scrollWidth', {
      value: 300,
      writable: true,
      configurable: true,
    });
    Object.defineProperty(contentContainer.element, 'clientWidth', {
      value: 100,
      writable: true,
      configurable: true,
    });

    // Trigger scroll to update state
    await contentContainer.trigger('scroll');
    await nextTick();
    await new Promise((resolve) => setTimeout(resolve, 200));

    const buttons = wrapper.findAllComponents(MIconButton);
    const nextButton = buttons[1];
    expect(nextButton.props('disabled')).toBe(false);
  });

  it('sets correct ARIA attributes on main container', () => {
    const wrapper = mountCarousel();
    const container = wrapper.find('.mc-carousel');
    expect(container.attributes('role')).toBe('group');
    expect(container.attributes('aria-roledescription')).toBe('carousel');
    // aria-labelledby should point to the headings wrapper (dynamic id)
    const labelledby = container.attributes('aria-labelledby');
    expect(labelledby).toBeDefined();
    expect(wrapper.find(`#${labelledby}`).exists()).toBe(true);
  });

  it('slide container has aria-live="polite"', () => {
    const wrapper = mountCarousel();
    const content = wrapper.find('.mc-carousel__content');
    expect(content.attributes('aria-live')).toBe('polite');
  });

  it('disables next button when there is only one child', async () => {
    const wrapper = mountCarousel({
      slots: {
        default: '<div class="slide">Slide 1</div>',
        header: '<h2 id="mc-carousel__title">Carousel Header</h2>',
      },
    });

    // Wait for component to initialize
    await nextTick();
    await new Promise((resolve) => setTimeout(resolve, 100));

    const buttons = wrapper.findAllComponents(MIconButton);
    const nextButton = buttons[1];

    // Next button should be disabled when there's only one child (which is the last)
    expect(nextButton.props('disabled')).toBe(true);
  });

  it('enables next button when there are multiple children and not on last', async () => {
    const wrapper = mountCarousel({
      slots: {
        default:
          '<div class="slide">Slide 1</div><div class="slide">Slide 2</div>',
        header: '<h2 id="mc-carousel__title">Carousel Header</h2>',
      },
    });

    // Mock container to simulate being on first slide with more content to scroll
    const contentContainer = wrapper.find('.mc-carousel__content');
    Object.defineProperty(contentContainer.element, 'scrollLeft', {
      value: 0,
      writable: true,
      configurable: true,
    });
    Object.defineProperty(contentContainer.element, 'scrollWidth', {
      value: 200,
      writable: true,
      configurable: true,
    });
    Object.defineProperty(contentContainer.element, 'clientWidth', {
      value: 100,
      writable: true,
      configurable: true,
    });

    // Trigger scroll to update state
    await contentContainer.trigger('scroll');
    await nextTick();
    await new Promise((resolve) => setTimeout(resolve, 200));

    const buttons = wrapper.findAllComponents(MIconButton);
    const nextButton = buttons[1];

    // Next button should be enabled when on first slide of two
    expect(nextButton.props('disabled')).toBe(false);
  });

  it('disables next button when on last child even with remaining scroll space', async () => {
    const wrapper = mountCarousel({
      slots: {
        default: '<div class="slide" style="width: 200px;">Wide Slide</div>',
        header: '<h2 id="mc-carousel__title">Carousel Header</h2>',
      },
    });

    // Mock container with a wide child that can still scroll but is the only/last child
    const contentContainer = wrapper.find('.mc-carousel__content');
    Object.defineProperty(contentContainer.element, 'scrollLeft', {
      value: 50,
      writable: true,
      configurable: true,
    });
    Object.defineProperty(contentContainer.element, 'scrollWidth', {
      value: 200,
      writable: true,
      configurable: true,
    });
    Object.defineProperty(contentContainer.element, 'clientWidth', {
      value: 100,
      writable: true,
      configurable: true,
    });

    // Trigger scroll to update state
    await contentContainer.trigger('scroll');
    await nextTick();
    await new Promise((resolve) => setTimeout(resolve, 200));

    const buttons = wrapper.findAllComponents(MIconButton);
    const nextButton = buttons[1];

    // Should be disabled because we're on the last (only) child
    // even though scrollLeft (50) < scrollWidth - clientWidth (100)
    expect(nextButton.props('disabled')).toBe(true);
  });
});
