/* @vitest-environment jsdom */

import { act, fireEvent, render, screen } from '@testing-library/react';
import type { ReactElement } from '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;
      wrist?: { x: number; y: number } | null;
    }>;
    primaryHand?: {
      x: number;
      y: number;
      state: string;
      side: string;
      wrist?: { x: number; y: number } | null;
    } | null;
    leftHand?: {
      x: number;
      y: number;
      state: string;
      side: string;
      wrist?: { x: number; y: number } | null;
    } | null;
    rightHand?: {
      x: number;
      y: number;
      state: string;
      side: string;
      wrist?: { x: number; y: number } | null;
    } | null;
    bodyCenter?: { x: number; y: number } | null;
    bodyJoints?: {
      leftShoulder?: { x: number; y: number } | null;
      rightShoulder?: { x: number; y: number } | null;
      leftElbow?: { x: number; y: number } | null;
      rightElbow?: { 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,
  }),
}));

vi.mock('../ResolvedAssetImage', () => ({
  ResolvedAssetImage: ({
    src,
    alt,
    className,
  }: {
    src?: string | null;
    alt?: string;
    className?: string;
  }) => (src ? <img src={src} alt={alt} className={className} /> : 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();
});

function setMocapBodyCenter(x: number) {
  mocapMock.command = {
    actions: [],
    bodyCenter: { x, y: 0.6 },
    hands: [],
    primaryHand: null,
    leftHand: null,
    rightHand: null,
  };
  mocapMock.receivedAtMs += 1;
}

async function advanceWarmupTime(ms: number) {
  await act(async () => {
    vi.advanceTimersByTime(ms);
  });
}

async function revealCurrentStepCue() {
  await advanceWarmupTime(1100);
}

async function completeCurrentPositionStepByHold() {
  await advanceWarmupTime(2200);
  await advanceWarmupTime(900);
}

async function completeCurrentNarrationStep() {
  await revealCurrentStepCue();
  await advanceWarmupTime(1000);
  await advanceWarmupTime(900);
}

async function sendMocapLeftHandTrack(
  rerender: (ui: ReactElement) => void,
  points: number[],
  options: { raised?: boolean } = {},
) {
  for (const x of points) {
    const y = options.raised ? 0.34 : 0.72;
    const wrist = { x, y };
    mocapMock.command = {
      actions: [],
      bodyCenter: { x: 0.5, y: 0.7 },
      bodyJoints: {
        leftShoulder: { x: 0.4, y: 0.42 },
        leftElbow: { x: 0.36, y: 0.5 },
      },
      hands: [{ x, y, state: 'unknown', side: 'left', wrist }],
      primaryHand: { x, y, state: 'unknown', side: 'left', wrist },
      leftHand: { x, y, state: 'unknown', side: 'left', wrist },
      rightHand: null,
    };
    mocapMock.receivedAtMs += 1;
    await act(async () => {
      rerender(<ChildMotionWarmupDemo />);
    });
  }
}

function setMocapCameraHandTrackPoint({
  cameraSide,
  x,
  y,
}: {
  cameraSide: 'left' | 'right';
  x: number;
  y: number;
}) {
  const wrist = { x, y };
  const hand = { x, y, state: 'unknown', side: cameraSide, wrist };
  const command = {
    actions: [],
    bodyCenter: { x: 0.5, y: 0.7 },
    bodyJoints: {
      leftShoulder: { x: 0.62, y: 0.48 },
      leftElbow: { x: 0.7, y: 0.5 },
      rightShoulder: { x: 0.38, y: 0.48 },
      rightElbow: { x: 0.3, y: 0.5 },
    },
    hands: [hand],
    primaryHand: hand,
    leftHand: null as null | typeof hand,
    rightHand: null as null | typeof hand,
  };

  if (cameraSide === 'left') {
    command.leftHand = hand;
  } else {
    command.rightHand = hand;
  }

  mocapMock.command = command;
  mocapMock.receivedAtMs += 1;
}

async function sendMocapCameraHandTrack(
  rerender: (ui: ReactElement) => void,
  cameraSide: 'left' | 'right',
  points: Array<{ x: number; y: number }>,
) {
  for (const point of points) {
    setMocapCameraHandTrackPoint({ cameraSide, ...point });
    await act(async () => {
      rerender(<ChildMotionWarmupDemo />);
    });
  }
}

async function sendPlayerLeftArmSwingTrack(
  rerender: (ui: ReactElement) => void,
) {
  await sendMocapCameraHandTrack(rerender, 'right', [
    { x: 0.2, y: 0.5 },
    { x: 0.16, y: 0.42 },
    { x: 0.13, y: 0.34 },
    { x: 0.15, y: 0.43 },
    { x: 0.19, y: 0.51 },
  ]);
}

async function sendPlayerRightArmSwingTrack(
  rerender: (ui: ReactElement) => void,
) {
  await sendMocapCameraHandTrack(rerender, 'left', [
    { x: 0.8, y: 0.5 },
    { x: 0.84, y: 0.42 },
    { x: 0.87, y: 0.34 },
    { x: 0.85, y: 0.43 },
    { x: 0.81, y: 0.51 },
  ]);
}

async function completeGreetingByWaveTrack(
  rerender: (ui: ReactElement) => void,
) {
  await sendMocapLeftHandTrack(rerender, [0.42, 0.51, 0.58, 0.49, 0.43], {
    raised: true,
  });
}

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.queryByLabelText('绿色圆环')).toBeNull();
  expect(screen.getByText('请横屏体验')).toBeTruthy();
});

test('shows narration first before revealing the step cue', async () => {
  vi.useFakeTimers();
  render(<ChildMotionWarmupDemo />);

  expect(screen.getByText('来到圆圈这里')).toBeTruthy();
  expect(screen.queryByLabelText('绿色圆环')).toBeNull();
  expect(screen.getByTestId('child-motion-stage').dataset.stepPhase).toBe('intro');

  await advanceWarmupTime(1000);

  expect(screen.getByLabelText('绿色圆环')).toBeTruthy();
  expect(screen.getByTestId('child-motion-stage').dataset.stepPhase).toBe('active');
});

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

  render(<ChildMotionWarmupDemo />);

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

test('start button opens the baby object match level', () => {
  markChildMotionWarmupCompletedInRuntime();

  render(<ChildMotionWarmupDemo />);

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

  expect(screen.getByTestId('baby-object-match-runtime')).toBeTruthy();
  expect(screen.getByText('将物品放入对应的篮子里')).toBeTruthy();
  expect(screen.queryByText('下一关正在设计中')).toBeNull();
});

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 dampens small jitter before moving the avatar', async () => {
  setMocapBodyCenter(0.5);
  const { rerender } = render(<ChildMotionWarmupDemo />);

  expect(screen.getByTestId('child-motion-avatar').getAttribute('style')).toContain(
    'left: 50%',
  );

  setMocapBodyCenter(0.508);
  await act(async () => {
    rerender(<ChildMotionWarmupDemo />);
  });
  expect(screen.getByTestId('child-motion-avatar').getAttribute('style')).toContain(
    'left: 50%',
  );

  setMocapBodyCenter(0.34);
  await act(async () => {
    rerender(<ChildMotionWarmupDemo />);
  });

  const style = screen.getByTestId('child-motion-avatar').getAttribute('style');
  expect(style).toContain('left: 46.5%');
  expect(style).not.toContain('left: 34%');
});

test('mocap body center keeps the warmup flow on the motion data source', async () => {
  vi.useFakeTimers();
  setMocapBodyCenter(0.5);
  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 revealCurrentStepCue();
  await completeCurrentPositionStepByHold();
  await vi.waitFor(() => {
    expect(screen.getByText('打个招呼')).toBeTruthy();
  });

  await revealCurrentStepCue();
  await completeGreetingByWaveTrack(rerender);
  await advanceWarmupTime(900);

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

  await completeCurrentNarrationStep();

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

  await revealCurrentStepCue();
  for (const targetX of [0.34, 0.34, 0.34, 0.34, 0.34]) {
    setMocapBodyCenter(targetX);
    await act(async () => {
      rerender(<ChildMotionWarmupDemo />);
    });
  }
  await vi.waitFor(() => {
    expect(screen.getByTestId('child-motion-avatar').getAttribute('style')).toContain(
      'left: 37',
    );
  });
  await completeCurrentPositionStepByHold();

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

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

test('mocap greeting requires a real horizontal wave track', async () => {
  vi.useFakeTimers();
  const { rerender, unmount } = render(<ChildMotionWarmupDemo />);

  await revealCurrentStepCue();
  await completeCurrentPositionStepByHold();
  await vi.waitFor(() => {
    expect(screen.getByText('打个招呼')).toBeTruthy();
  });

  await revealCurrentStepCue();
  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 advanceWarmupTime(900);
  expect(screen.getByRole('heading', { name: '打个招呼' })).toBeTruthy();

  await sendMocapLeftHandTrack(rerender, [0.42, 0.51, 0.58, 0.49, 0.43], {
    raised: false,
  });
  await advanceWarmupTime(900);
  expect(screen.getByRole('heading', { name: '打个招呼' })).toBeTruthy();

  for (const x of [0.42, 0.51, 0.58, 0.49, 0.43]) {
    const wrist = { x, y: 0.34 };
    mocapMock.command = {
      actions: [],
      bodyCenter: { x: 0.5, y: 0.7 },
      hands: [{ x, y: 0.34, state: 'unknown', side: 'left', wrist }],
      primaryHand: { x, y: 0.34, state: 'unknown', side: 'left', wrist },
      leftHand: { x, y: 0.34, state: 'unknown', side: 'left', wrist },
      rightHand: null,
    };
    mocapMock.receivedAtMs += 1;
    await act(async () => {
      rerender(<ChildMotionWarmupDemo />);
    });
  }
  await advanceWarmupTime(900);
  expect(screen.getByRole('heading', { name: '打个招呼' })).toBeTruthy();

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

test('mocap arm swing steps require body-side mapping and vertical open arm motion', async () => {
  vi.useFakeTimers();
  const { rerender, unmount } = render(<ChildMotionWarmupDemo />);

  const advancePositionStep = async (key: string, code: string) => {
    await revealCurrentStepCue();
    await act(async () => {
      fireEvent.keyDown(window, { key, code });
    });
    await completeCurrentPositionStepByHold();
    await act(async () => {
      fireEvent.keyUp(window, { key, code });
    });
  };

  await revealCurrentStepCue();
  await completeCurrentPositionStepByHold();
  await vi.waitFor(() => {
    expect(screen.getByText('打个招呼')).toBeTruthy();
  });

  await revealCurrentStepCue();
  await completeGreetingByWaveTrack(rerender);

  await advanceWarmupTime(900);
  await completeCurrentNarrationStep();
  await advancePositionStep('a', 'KeyA');
  await revealCurrentStepCue();
  await completeCurrentPositionStepByHold();
  await advancePositionStep('d', 'KeyD');
  await revealCurrentStepCue();
  await completeCurrentPositionStepByHold();
  await vi.waitFor(() => {
    expect(screen.getByRole('heading', { name: '挥动左手' })).toBeTruthy();
  });

  await revealCurrentStepCue();
  await sendMocapCameraHandTrack(rerender, 'left', [
    { x: 0.78, y: 0.5 },
    { x: 0.86, y: 0.5 },
    { x: 0.79, y: 0.5 },
    { x: 0.87, y: 0.5 },
    { x: 0.8, y: 0.5 },
  ]);
  await advanceWarmupTime(900);
  expect(screen.getByRole('heading', { name: '挥动左手' })).toBeTruthy();

  await sendMocapCameraHandTrack(rerender, 'right', [
    { x: 0.32, y: 0.74 },
    { x: 0.24, y: 0.74 },
    { x: 0.31, y: 0.74 },
    { x: 0.23, y: 0.74 },
    { x: 0.3, y: 0.74 },
  ]);
  await advanceWarmupTime(900);
  expect(screen.getByRole('heading', { name: '挥动左手' })).toBeTruthy();

  await sendPlayerLeftArmSwingTrack(rerender);

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

  await revealCurrentStepCue();
  await sendMocapCameraHandTrack(rerender, 'right', [
    { x: 0.2, y: 0.5 },
    { x: 0.16, y: 0.42 },
    { x: 0.13, y: 0.34 },
    { x: 0.15, y: 0.43 },
    { x: 0.19, y: 0.51 },
  ]);
  await advanceWarmupTime(900);
  expect(screen.getByRole('heading', { name: '挥动右手' })).toBeTruthy();

  await sendPlayerRightArmSwingTrack(rerender);

  await advanceWarmupTime(900);
  await vi.waitFor(() => {
    expect(screen.getByRole('heading', { name: '原地跳一下' })).toBeTruthy();
  });
  await advanceWarmupTime(720);
  await act(async () => {
    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();
});
