import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import MobileBottomSheet from './MobileBottomSheet';

class FakeVisualViewport extends EventTarget {
  height: number;
  width = 390;
  offsetTop = 0;
  offsetLeft = 0;
  pageTop = 0;
  pageLeft = 0;
  scale = 1;
  onresize: ((this: VisualViewport, ev: Event) => unknown) | null = null;
  onscroll: ((this: VisualViewport, ev: Event) => unknown) | null = null;

  constructor(height: number) {
    super();
    this.height = height;
  }
}

const originalInnerHeight = window.innerHeight;
const originalVisualViewport = window.visualViewport;
const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
const originalSetPointerCapture = HTMLElement.prototype.setPointerCapture;

function installViewport({
  innerHeight,
  visualHeight,
}: {
  innerHeight: number;
  visualHeight: number;
}) {
  const visualViewport = new FakeVisualViewport(visualHeight);

  Object.defineProperty(window, 'innerHeight', {
    configurable: true,
    writable: true,
    value: innerHeight,
  });
  Object.defineProperty(window, 'visualViewport', {
    configurable: true,
    value: visualViewport as unknown as VisualViewport,
  });

  return visualViewport;
}

function renderSheet() {
  const coveredHeights: number[] = [];
  const view = render(
    <MobileBottomSheet onCoveredHeightChange={(height) => coveredHeights.push(height)}>
      <label>
        Name
        <input aria-label="Name" />
      </label>
      <button type="button">Apply</button>
    </MobileBottomSheet>
  );
  const sheet = view.container.querySelector('section');
  if (!(sheet instanceof HTMLElement)) throw new Error('Expected bottom sheet section');

  return { ...view, coveredHeights, sheet };
}

describe('MobileBottomSheet keyboard avoidance', () => {
  beforeEach(() => {
    HTMLElement.prototype.scrollIntoView = vi.fn();
    HTMLElement.prototype.setPointerCapture = vi.fn();
  });

  afterEach(() => {
    cleanup();
    Object.defineProperty(window, 'innerHeight', {
      configurable: true,
      writable: true,
      value: originalInnerHeight,
    });
    Object.defineProperty(window, 'visualViewport', {
      configurable: true,
      value: originalVisualViewport,
    });
    Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
      configurable: true,
      value: originalScrollIntoView,
    });
    Object.defineProperty(HTMLElement.prototype, 'setPointerCapture', {
      configurable: true,
      value: originalSetPointerCapture,
    });
  });

  it('ignores visual viewport keyboard inset until a sheet text field is focused', () => {
    const visualViewport = installViewport({ innerHeight: 800, visualHeight: 500 });
    const { sheet } = renderSheet();

    expect(sheet.style.bottom).toBe('0px');

    act(() => {
      visualViewport.dispatchEvent(new Event('resize'));
    });

    expect(sheet.style.bottom).toBe('0px');
  });

  it('clears keyboard offset when focus leaves even if visual viewport is stale', async () => {
    installViewport({ innerHeight: 800, visualHeight: 500 });
    const { sheet } = renderSheet();
    const input = screen.getByLabelText('Name');

    await act(async () => {
      input.focus();
    });
    expect(sheet.style.bottom).toBe('300px');

    await act(async () => {
      input.blur();
    });
    expect(sheet.style.bottom).toBe('0px');
  });

  it('leaves keyboard avoidance mode when tapping non-editable sheet content', async () => {
    installViewport({ innerHeight: 800, visualHeight: 500 });
    const { sheet } = renderSheet();
    const input = screen.getByLabelText('Name');

    await act(async () => {
      input.focus();
    });
    expect(sheet.style.bottom).toBe('300px');

    await act(async () => {
      fireEvent.pointerDown(screen.getByRole('button', { name: 'Apply' }));
    });

    expect(document.activeElement).not.toBe(input);
    expect(sheet.style.bottom).toBe('0px');
  });

  it('reports covered height while the drawer is being dragged', async () => {
    installViewport({ innerHeight: 800, visualHeight: 800 });
    const { coveredHeights, sheet } = renderSheet();
    const handle = sheet.firstElementChild?.firstElementChild;

    if (!(handle instanceof HTMLElement)) throw new Error('Expected bottom sheet drag handle');

    expect(coveredHeights[coveredHeights.length - 1]).toBe(352);

    await act(async () => {
      fireEvent.pointerDown(handle, { pointerId: 1, clientY: 500 });
      fireEvent.pointerMove(handle, { pointerId: 1, clientY: 400 });
    });

    expect(coveredHeights[coveredHeights.length - 1]).toBe(452);
  });

  it('caps the height so the drag handle never slides under a top header', async () => {
    installViewport({ innerHeight: 800, visualHeight: 800 });
    const { coveredHeights, sheet, container } = renderSheet();
    const handle = sheet.firstElementChild?.firstElementChild;

    if (!(handle instanceof HTMLElement)) throw new Error('Expected bottom sheet drag handle');

    // Pretend a 48px app header sits above the sheet's layout parent, so the
    // sheet must reserve that strip (plus a small gap) at the top of the window.
    container.getBoundingClientRect = () =>
      ({
        top: 48,
        bottom: 800,
        left: 0,
        right: 390,
        width: 390,
        height: 752,
        x: 0,
        y: 48,
        toJSON: () => ({}),
      }) as DOMRect;

    await act(async () => {
      window.dispatchEvent(new Event('resize'));
    });

    // Drag the handle far above the top of the window.
    await act(async () => {
      fireEvent.pointerDown(handle, { pointerId: 1, clientY: 700 });
      fireEvent.pointerMove(handle, { pointerId: 1, clientY: -400 });
    });

    // 800 (viewport) - 48 (header) - 16 (gap) = 736 px ceiling; never taller.
    expect(coveredHeights[coveredHeights.length - 1]).toBe(736);
  });
});
