/* @vitest-environment jsdom */

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

import { ChildMotionWarmupDemo } from './ChildMotionWarmupDemo';
import {
  markChildMotionWarmupCompletedInRuntime,
  resetChildMotionWarmupRuntimeSession,
} from './childMotionWarmupModel';

const mocapMock = vi.hoisted(() => ({
  status: 'connected' as 'idle' | 'connecting' | 'connected' | 'error',
  command: null as null | {
    actions: string[];
    hands?: Array<{ x: number; y: number; state: string; side: string }>;
    primaryHand?: { x: number; y: number; state: string; side: string } | null;
    leftHand?: { x: number; y: number; state: string; side: string } | null;
    rightHand?: { x: number; y: number; state: string; side: string } | null;
    bodyCenter?: { x: number; y: number } | null;
  },
  receivedAtMs: 1,
}));

vi.mock('../../services/useMocapInput', () => ({
  useMocapInput: ({ enabled }: { enabled: boolean }) => ({
    status: enabled ? mocapMock.status : 'idle',
    latestCommand: enabled ? mocapMock.command : null,
    rawPacketPreview:
      enabled && mocapMock.command
        ? {
            text: JSON.stringify(mocapMock.command),
            receivedAtMs: mocapMock.receivedAtMs,
          }
        : null,
    error: null,
  }),
}));

beforeEach(() => {
  resetChildMotionWarmupRuntimeSession();
  vi.restoreAllMocks();
  mocapMock.status = 'connected';
  mocapMock.command = null;
  mocapMock.receivedAtMs = 1;
  Object.defineProperty(navigator, 'mediaDevices', {
    configurable: true,
    value: undefined,
  });
});

afterEach(() => {
  vi.useRealTimers();
  vi.restoreAllMocks();
});

test('renders the warmup stage and starts with the center ring step', () => {
  render(<ChildMotionWarmupDemo />);

  expect(screen.getByTestId('child-motion-demo')).toBeTruthy();
  expect(screen.getByText('来到圆圈这里')).toBeTruthy();
  expect(screen.getByLabelText('绿色圆环')).toBeTruthy();
  expect(screen.getByText('请横屏体验')).toBeTruthy();
});

test('re-entering within the same runtime session opens the start button', () => {
  markChildMotionWarmupCompletedInRuntime();

  render(<ChildMotionWarmupDemo />);

  expect(screen.getByRole('button', { name: '开始游戏' })).toBeTruthy();
});

test('developer keyboard input moves the avatar and triggers jump state', () => {
  render(<ChildMotionWarmupDemo />);

  const avatar = screen.getByTestId('child-motion-avatar');

  fireEvent.keyDown(window, { key: 'a', code: 'KeyA' });
  expect(avatar.getAttribute('style')).toContain('left: 34%');

  fireEvent.keyDown(window, { key: 'd', code: 'KeyD' });
  expect(avatar.getAttribute('style')).toContain('left: 66%');

  fireEvent.keyUp(window, { key: 'd', code: 'KeyD' });
  expect(avatar.getAttribute('style')).toContain('left: 50%');

  fireEvent.keyDown(window, { key: ' ', code: 'Space' });
  expect(avatar.className).toContain('child-motion-avatar--jumping');
});

test('mocap body center keeps the warmup flow on the motion data source', async () => {
  vi.useFakeTimers();
  mocapMock.command = {
    actions: [],
    bodyCenter: { x: 0.5, y: 0.6 },
    hands: [],
    primaryHand: null,
    leftHand: null,
    rightHand: null,
  };
  const { rerender, unmount } = render(<ChildMotionWarmupDemo />);

  expect(screen.queryByText('摄像头暂不可用，已切换到本地演示')).toBeNull();
  expect(screen.queryByText('动作数据已连接，等待识别')).toBeNull();
  expect(screen.getByTestId('child-motion-avatar').getAttribute('style')).toContain(
    'left: 50%',
  );

  await act(async () => {
    vi.advanceTimersByTime(2100);
    await vi.runOnlyPendingTimersAsync();
  });
  await vi.waitFor(() => {
    expect(screen.getByText('打个招呼')).toBeTruthy();
  });

  mocapMock.command = {
    actions: ['open_palm'],
    bodyCenter: { x: 0.5, y: 0.6 },
    hands: [{ x: 0.48, y: 0.34, state: 'open_palm', side: 'left' }],
    primaryHand: { x: 0.48, y: 0.34, state: 'open_palm', side: 'left' },
    leftHand: { x: 0.48, y: 0.34, state: 'open_palm', side: 'left' },
    rightHand: null,
  };
  mocapMock.receivedAtMs += 1;
  await act(async () => {
    rerender(<ChildMotionWarmupDemo />);
    vi.advanceTimersByTime(1000);
    await vi.runOnlyPendingTimersAsync();
  });

  await vi.waitFor(() => {
    expect(screen.getByText('准备热身')).toBeTruthy();
  });

  await act(async () => {
    vi.advanceTimersByTime(1000);
    await vi.runOnlyPendingTimersAsync();
  });

  await vi.waitFor(() => {
    expect(screen.getByRole('heading', { name: '向左一步' })).toBeTruthy();
  });

  mocapMock.command = {
    actions: [],
    bodyCenter: { x: 0.34, y: 0.6 },
    hands: [],
    primaryHand: null,
    leftHand: null,
    rightHand: null,
  };
  mocapMock.receivedAtMs += 1;
  await act(async () => {
    rerender(<ChildMotionWarmupDemo />);
  });
  await vi.waitFor(() => {
    expect(screen.getByTestId('child-motion-avatar').getAttribute('style')).toContain(
      'left: 34%',
    );
  });
  await act(async () => {
    vi.advanceTimersByTime(2100);
    await vi.runOnlyPendingTimersAsync();
  });

  await vi.waitFor(() => {
    expect(screen.getByRole('heading', { name: '回到中间来' })).toBeTruthy();
  });

  await act(async () => {
    unmount();
  });
  vi.useRealTimers();
});

test('mocap open palm completes the greeting wave step', async () => {
  vi.useFakeTimers();
  const { rerender, unmount } = render(<ChildMotionWarmupDemo />);

  await act(async () => {
    vi.advanceTimersByTime(2100);
    await vi.runOnlyPendingTimersAsync();
  });
  await vi.waitFor(() => {
    expect(screen.getByText('打个招呼')).toBeTruthy();
  });

  mocapMock.command = {
    actions: ['open_palm'],
    hands: [{ x: 0.46, y: 0.34, state: 'open_palm', side: 'left' }],
    primaryHand: { x: 0.46, y: 0.34, state: 'open_palm', side: 'left' },
    leftHand: { x: 0.46, y: 0.34, state: 'open_palm', side: 'left' },
    rightHand: null,
  };
  mocapMock.receivedAtMs += 1;
  await act(async () => {
    rerender(<ChildMotionWarmupDemo />);
  });

  await vi.waitFor(() => {
    expect(screen.getByText('准备热身')).toBeTruthy();
  });
  await act(async () => {
    unmount();
  });
  vi.useRealTimers();
});

test('mocap hand tracks complete left and right wave steps only after movement is visible', async () => {
  vi.useFakeTimers();
  const { rerender, unmount } = render(<ChildMotionWarmupDemo />);

  const advancePositionStep = async (key: string, code: string) => {
    await act(async () => {
      fireEvent.keyDown(window, { key, code });
    });
    await act(async () => {
      vi.advanceTimersByTime(2100);
      await vi.runOnlyPendingTimersAsync();
    });
    await act(async () => {
      fireEvent.keyUp(window, { key, code });
    });
  };

  await act(async () => {
    vi.advanceTimersByTime(2100);
    await vi.runOnlyPendingTimersAsync();
  });
  await vi.waitFor(() => {
    expect(screen.getByText('打个招呼')).toBeTruthy();
  });

  mocapMock.command = {
    actions: ['open_palm'],
    hands: [{ x: 0.48, y: 0.34, state: 'open_palm', side: 'left' }],
    primaryHand: { x: 0.48, y: 0.34, state: 'open_palm', side: 'left' },
    leftHand: { x: 0.48, y: 0.34, state: 'open_palm', side: 'left' },
    rightHand: null,
  };
  mocapMock.receivedAtMs += 1;
  await act(async () => {
    rerender(<ChildMotionWarmupDemo />);
  });

  await act(async () => {
    vi.advanceTimersByTime(1000);
    await vi.runOnlyPendingTimersAsync();
  });
  await advancePositionStep('a', 'KeyA');
  await act(async () => {
    vi.advanceTimersByTime(120);
    await vi.runOnlyPendingTimersAsync();
    vi.advanceTimersByTime(2100);
    await vi.runOnlyPendingTimersAsync();
  });
  await advancePositionStep('d', 'KeyD');
  await act(async () => {
    vi.advanceTimersByTime(120);
    await vi.runOnlyPendingTimersAsync();
    vi.advanceTimersByTime(2100);
    await vi.runOnlyPendingTimersAsync();
  });
  await vi.waitFor(() => {
    expect(screen.getByRole('heading', { name: '挥动左手' })).toBeTruthy();
  });

  mocapMock.command = {
    actions: [],
    leftHand: { x: 0.3, y: 0.38, state: 'unknown', side: 'left' },
    primaryHand: { x: 0.3, y: 0.38, state: 'unknown', side: 'left' },
    rightHand: null,
  };
  mocapMock.receivedAtMs += 1;
  await act(async () => {
    rerender(<ChildMotionWarmupDemo />);
  });
  mocapMock.command = {
    actions: [],
    leftHand: { x: 0.39, y: 0.36, state: 'unknown', side: 'left' },
    primaryHand: { x: 0.39, y: 0.36, state: 'unknown', side: 'left' },
    rightHand: null,
  };
  mocapMock.receivedAtMs += 1;
  await act(async () => {
    rerender(<ChildMotionWarmupDemo />);
  });
  mocapMock.command = {
    actions: [],
    leftHand: { x: 0.31, y: 0.34, state: 'unknown', side: 'left' },
    primaryHand: { x: 0.31, y: 0.34, state: 'unknown', side: 'left' },
    rightHand: null,
  };
  mocapMock.receivedAtMs += 1;
  await act(async () => {
    rerender(<ChildMotionWarmupDemo />);
  });

  await vi.waitFor(() => {
    expect(screen.getByRole('heading', { name: '挥动右手' })).toBeTruthy();
  });

  mocapMock.command = {
    actions: ['right_hand_wave'],
    leftHand: null,
    primaryHand: { x: 0.64, y: 0.35, state: 'unknown', side: 'right' },
    rightHand: { x: 0.64, y: 0.35, state: 'unknown', side: 'right' },
  };
  mocapMock.receivedAtMs += 1;
  await act(async () => {
    rerender(<ChildMotionWarmupDemo />);
  });

  await vi.waitFor(() => {
    expect(screen.getByRole('heading', { name: '原地跳一下' })).toBeTruthy();
  });
  await act(async () => {
    vi.advanceTimersByTime(720);
    await vi.runOnlyPendingTimersAsync();
    unmount();
  });
  vi.useRealTimers();
});

test('connects camera stream and releases it on unmount', async () => {
  const stopTrack = vi.fn();
  const stream = {
    getTracks: () => [
      {
        stop: stopTrack,
      },
    ],
  } as unknown as MediaStream;
  const getUserMedia = vi.fn().mockResolvedValue(stream);
  const play = vi
    .spyOn(HTMLMediaElement.prototype, 'play')
    .mockResolvedValue(undefined);
  Object.defineProperty(navigator, 'mediaDevices', {
    configurable: true,
    value: {
      getUserMedia,
    },
  });

  const { unmount } = render(<ChildMotionWarmupDemo />);

  expect(screen.queryByText('摄像头暂不可用，已切换到本地演示')).toBeNull();
  expect(screen.getByText('动作数据已连接，等待识别')).toBeTruthy();
  await vi.waitFor(() => {
    expect(getUserMedia).toHaveBeenCalledWith({
      audio: false,
      video: {
        facingMode: 'user',
      },
    });
    expect(play).toHaveBeenCalled();
  });

  unmount();

  expect(stopTrack).toHaveBeenCalled();
});
