/* @vitest-environment jsdom */

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

import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import {
  BABY_OBJECT_MATCH_EDUTAINMENT_TAG,
  BABY_OBJECT_MATCH_TEMPLATE_ID,
  BABY_OBJECT_MATCH_TEMPLATE_NAME,
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type { UseMocapInputResult } from '../../services/useMocapInput';
import { BabyObjectMatchRuntimeShell } from './BabyObjectMatchRuntimeShell';

vi.mock('../ResolvedAssetImage', () => ({
  ResolvedAssetImage: ({
    src,
    alt,
    className,
    'data-testid': dataTestId,
  }: {
    src?: string | null;
    alt?: string;
    className?: string;
    'data-testid'?: string;
  }) =>
    src ? (
      <img
        src={src}
        alt={alt}
        className={className}
        data-testid={dataTestId}
      />
    ) : null,
}));

vi.mock('../../services/useMocapInput', () => ({
  useMocapInput: () => ({
    status: 'idle',
    latestCommand: null,
    rawPacketPreview: null,
    error: null,
  }),
}));

function createDraft(): BabyObjectMatchDraft {
  return {
    draftId: 'baby-object-draft-1',
    profileId: 'baby-object-profile-1',
    templateId: BABY_OBJECT_MATCH_TEMPLATE_ID,
    templateName: BABY_OBJECT_MATCH_TEMPLATE_NAME,
    workTitle: '宝贝识物',
    workDescription: '苹果和香蕉识物分类',
    itemNames: ['苹果', '香蕉'],
    itemAssets: [
      {
        itemId: 'baby-object-item-1',
        itemName: '苹果',
        imageSrc: 'data:image/svg+xml;utf8,apple',
        assetObjectId: null,
        generationProvider: 'placeholder',
        prompt: '苹果',
      },
      {
        itemId: 'baby-object-item-2',
        itemName: '香蕉',
        imageSrc: 'data:image/svg+xml;utf8,banana',
        assetObjectId: null,
        generationProvider: 'placeholder',
        prompt: '香蕉',
      },
    ],
    visualPackage: null,
    themeTags: [BABY_OBJECT_MATCH_EDUTAINMENT_TAG, '宝贝识物'],
    publicationStatus: 'published',
    createdAt: '2026-05-11T00:00:00.000Z',
    updatedAt: '2026-05-11T00:00:00.000Z',
    publishedAt: '2026-05-11T00:00:00.000Z',
  };
}

function createVisualPackageDraft(): BabyObjectMatchDraft {
  return {
    ...createDraft(),
    visualPackage: {
      themePrompt: '果园主题',
      assets: [
        {
          assetId: 'baby-object-visual-background',
          assetKind: 'background',
          imageSrc: 'data:image/png;base64,background',
          assetObjectId: null,
          generationProvider: 'vector-engine-gpt-image-2',
          prompt: '背景',
        },
        {
          assetId: 'baby-object-visual-ui-frame',
          assetKind: 'ui-frame',
          imageSrc: 'data:image/png;base64,ui',
          assetObjectId: null,
          generationProvider: 'vector-engine-gpt-image-2',
          prompt: 'UI',
        },
        {
          assetId: 'baby-object-visual-gift-box',
          assetKind: 'gift-box',
          imageSrc: 'data:image/png;base64,gift',
          assetObjectId: null,
          generationProvider: 'vector-engine-gpt-image-2',
          prompt: '礼盒',
        },
        {
          assetId: 'baby-object-visual-basket',
          assetKind: 'basket',
          imageSrc: 'data:image/png;base64,basket',
          assetObjectId: null,
          generationProvider: 'vector-engine-gpt-image-2',
          prompt: '篮子',
        },
        {
          assetId: 'baby-object-visual-smoke-puff',
          assetKind: 'smoke-puff',
          imageSrc: 'data:image/png;base64,smoke',
          assetObjectId: null,
          generationProvider: 'vector-engine-gpt-image-2',
          prompt: '烟雾',
        },
      ],
    },
  };
}

function createMocapInput(
  overrides: Partial<UseMocapInputResult> = {},
): UseMocapInputResult {
  return {
    status: 'connected',
    latestCommand: null,
    rawPacketPreview: null,
    error: null,
    ...overrides,
  };
}

function createRandomSequence(values: number[]) {
  let index = 0;
  return () => {
    const value = values[index] ?? values[values.length - 1] ?? 0;
    index += 1;
    return value;
  };
}

function dispatchPointerEvent(
  target: HTMLElement,
  type: string,
  options: {
    pointerId: number;
    button?: number;
    clientX: number;
    clientY: number;
  },
) {
  const event = new Event(type, { bubbles: true, cancelable: true });
  Object.assign(event, options);
  target.dispatchEvent(event);
}

function dragHand(stage: HTMLElement, button: 0 | 2) {
  Object.defineProperty(stage, 'getBoundingClientRect', {
    configurable: true,
    value: () => ({
      x: 0,
      y: 0,
      left: 0,
      top: 0,
      right: 320,
      bottom: 240,
      width: 320,
      height: 240,
      toJSON: () => ({}),
    }),
  });

  act(() => {
    dispatchPointerEvent(stage, 'pointerdown', {
      pointerId: button + 1,
      button,
      clientX: 20,
      clientY: 140,
    });
  });
  act(() => {
    dispatchPointerEvent(stage, 'pointermove', {
      pointerId: button + 1,
      button,
      clientX: 120,
      clientY: 140,
    });
  });
  act(() => {
    dispatchPointerEvent(stage, 'pointerup', {
      pointerId: button + 1,
      button,
      clientX: 120,
      clientY: 140,
    });
  });
}

async function advanceRoundIntro() {
  await act(async () => {
    await vi.advanceTimersByTimeAsync(620);
  });
  await act(async () => {
    await vi.advanceTimersByTimeAsync(640);
  });
  await act(async () => {
    await vi.advanceTimersByTimeAsync(620);
  });
}

async function advanceFeedback() {
  await act(async () => {
    await vi.advanceTimersByTimeAsync(1200);
  });
}

test('shows the first gift item after gift and item animations', async () => {
  vi.useFakeTimers();
  render(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={createRandomSequence([0, 0])}
    />,
  );

  expect(screen.getByText('将物品放入对应的篮子里')).toBeTruthy();
  expect(screen.getByTestId('baby-object-current-item').textContent).toBe('');

  await advanceRoundIntro();

  expect(
    within(screen.getByTestId('baby-object-current-item')).getByAltText('苹果'),
  ).toBeTruthy();
  vi.useRealTimers();
});

test('applies generated visual package to stage, gift box, baskets, smoke and hud', async () => {
  vi.useFakeTimers();
  const { container } = render(
    <BabyObjectMatchRuntimeShell draft={createVisualPackageDraft()} />,
  );
  const stage = container.querySelector('.baby-object-runtime__stage');
  if (!(stage instanceof HTMLElement)) {
    throw new Error('Missing baby object runtime stage');
  }

  expect(stage.classList.contains('baby-object-runtime__stage--skinned')).toBe(
    true,
  );
  expect(
    screen
      .getByTestId('baby-object-background-image')
      .getAttribute('src'),
  ).toBe('data:image/png;base64,background');
  expect(
    stage.style.getPropertyValue('--baby-object-ui-frame-image'),
  ).toContain('ui');
  expect(stage.style.getPropertyValue('--baby-object-smoke-image')).toContain(
    'smoke',
  );
  expect(screen.getByAltText('礼物盒')).toBeTruthy();
  expect(
    container.querySelector('.baby-object-runtime__basket-shell-image'),
  ).toBeTruthy();

  await act(async () => {
    await vi.advanceTimersByTimeAsync(620);
  });

  expect(screen.getByTestId('baby-object-smoke-effect')).toBeTruthy();
  vi.useRealTimers();
});

test('removes the gift box after smoke releases the current item', async () => {
  vi.useFakeTimers();
  render(
    <BabyObjectMatchRuntimeShell
      draft={createVisualPackageDraft()}
      random={createRandomSequence([0])}
    />,
  );

  expect(screen.getByLabelText('礼物盒')).toBeTruthy();

  await act(async () => {
    await vi.advanceTimersByTimeAsync(620);
  });

  expect(screen.getByLabelText('礼物盒')).toBeTruthy();
  expect(screen.getByTestId('baby-object-smoke-effect')).toBeTruthy();

  await act(async () => {
    await vi.advanceTimersByTimeAsync(640);
  });

  expect(screen.queryByLabelText('礼物盒')).toBeNull();
  expect(screen.getByTestId('baby-object-smoke-effect')).toBeTruthy();

  await act(async () => {
    await vi.advanceTimersByTimeAsync(620);
  });

  expect(screen.queryByLabelText('礼物盒')).toBeNull();
  expect(
    within(screen.getByTestId('baby-object-current-item')).getByAltText('苹果'),
  ).toBeTruthy();
  vi.useRealTimers();
});

test('keeps left and right baskets fixed while only the gift item is random', async () => {
  vi.useFakeTimers();
  render(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={createRandomSequence([0.99])}
    />,
  );

  await advanceRoundIntro();

  expect(
    within(screen.getByTestId('baby-object-current-item')).getByAltText('香蕉'),
  ).toBeTruthy();
  expect(screen.getByLabelText('左侧篮子 苹果')).toBeTruthy();
  expect(screen.getByLabelText('右侧篮子 香蕉')).toBeTruthy();
  vi.useRealTimers();
});

test('mocap camera-right hand movement sends the player left hand item into the left basket', async () => {
  vi.useFakeTimers();
  const random = createRandomSequence([0, 0]);
  const { rerender } = render(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={random}
      mocapInput={createMocapInput()}
    />,
  );

  await advanceRoundIntro();

  rerender(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={random}
      mocapInput={createMocapInput({
        latestCommand: {
          actions: [],
          hands: [{ x: 0.22, y: 0.45, state: 'open_palm', side: 'right' }],
          primaryHand: { x: 0.22, y: 0.45, state: 'open_palm', side: 'right' },
          leftHand: null,
          rightHand: { x: 0.22, y: 0.45, state: 'open_palm', side: 'right' },
        },
        rawPacketPreview: {
          text: 'camera-right-horizontal-1',
          receivedAtMs: 1,
        },
      })}
    />,
  );

  rerender(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={random}
      mocapInput={createMocapInput({
        latestCommand: {
          actions: [],
          hands: [{ x: 0.24, y: 0.45, state: 'open_palm', side: 'right' }],
          primaryHand: { x: 0.24, y: 0.45, state: 'open_palm', side: 'right' },
          leftHand: null,
          rightHand: { x: 0.24, y: 0.45, state: 'open_palm', side: 'right' },
        },
        rawPacketPreview: {
          text: 'camera-right-horizontal-2',
          receivedAtMs: 2,
        },
      })}
    />,
  );

  rerender(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={random}
      mocapInput={createMocapInput({
        latestCommand: {
          actions: [],
          hands: [{ x: 0.22, y: 0.45, state: 'open_palm', side: 'right' }],
          primaryHand: { x: 0.22, y: 0.45, state: 'open_palm', side: 'right' },
          leftHand: null,
          rightHand: { x: 0.22, y: 0.45, state: 'open_palm', side: 'right' },
        },
        rawPacketPreview: {
          text: 'camera-right-horizontal-3',
          receivedAtMs: 3,
        },
      })}
    />,
  );

  expect(screen.queryByText('真棒')).toBeNull();

  rerender(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={random}
      mocapInput={createMocapInput({
        latestCommand: {
          actions: [],
          hands: [{ x: 0.31, y: 0.45, state: 'open_palm', side: 'right' }],
          primaryHand: { x: 0.31, y: 0.45, state: 'open_palm', side: 'right' },
          leftHand: null,
          rightHand: { x: 0.31, y: 0.45, state: 'open_palm', side: 'right' },
        },
        rawPacketPreview: {
          text: 'camera-right-horizontal-4',
          receivedAtMs: 4,
        },
      })}
    />,
  );

  expect(screen.getByText('真棒')).toBeTruthy();
  expect(screen.getByLabelText('成功次数').textContent).toBe('1/20');
  vi.useRealTimers();
});

test('mocap camera-left hand movement sends the player right hand item into the right basket', async () => {
  vi.useFakeTimers();
  const random = createRandomSequence([0, 0]);
  const { rerender } = render(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={random}
      mocapInput={createMocapInput()}
    />,
  );

  await advanceRoundIntro();

  rerender(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={random}
      mocapInput={createMocapInput({
        latestCommand: {
          actions: [],
          hands: [{ x: 0.82, y: 0.45, state: 'open_palm', side: 'left' }],
          primaryHand: { x: 0.82, y: 0.45, state: 'open_palm', side: 'left' },
          leftHand: { x: 0.82, y: 0.45, state: 'open_palm', side: 'left' },
          rightHand: null,
        },
        rawPacketPreview: { text: 'camera-left-horizontal-1', receivedAtMs: 1 },
      })}
    />,
  );

  rerender(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={random}
      mocapInput={createMocapInput({
        latestCommand: {
          actions: [],
          hands: [{ x: 0.8, y: 0.45, state: 'open_palm', side: 'left' }],
          primaryHand: { x: 0.8, y: 0.45, state: 'open_palm', side: 'left' },
          leftHand: { x: 0.8, y: 0.45, state: 'open_palm', side: 'left' },
          rightHand: null,
        },
        rawPacketPreview: { text: 'camera-left-horizontal-2', receivedAtMs: 2 },
      })}
    />,
  );

  rerender(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={random}
      mocapInput={createMocapInput({
        latestCommand: {
          actions: [],
          hands: [{ x: 0.82, y: 0.45, state: 'open_palm', side: 'left' }],
          primaryHand: { x: 0.82, y: 0.45, state: 'open_palm', side: 'left' },
          leftHand: { x: 0.82, y: 0.45, state: 'open_palm', side: 'left' },
          rightHand: null,
        },
        rawPacketPreview: { text: 'camera-left-horizontal-3', receivedAtMs: 3 },
      })}
    />,
  );

  expect(screen.queryByText('再想一想吧')).toBeNull();

  rerender(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={random}
      mocapInput={createMocapInput({
        latestCommand: {
          actions: [],
          hands: [{ x: 0.73, y: 0.45, state: 'open_palm', side: 'left' }],
          primaryHand: { x: 0.73, y: 0.45, state: 'open_palm', side: 'left' },
          leftHand: { x: 0.73, y: 0.45, state: 'open_palm', side: 'left' },
          rightHand: null,
        },
        rawPacketPreview: { text: 'camera-left-horizontal-4', receivedAtMs: 4 },
      })}
    />,
  );

  expect(screen.getByText('再想一想吧')).toBeTruthy();
  expect(screen.getByLabelText('成功次数').textContent).toBe('0/20');
  vi.useRealTimers();
});

test('mocap action names do not select a basket without horizontal hand movement', async () => {
  vi.useFakeTimers();
  const random = createRandomSequence([0, 0]);
  const { rerender } = render(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={random}
      mocapInput={createMocapInput()}
    />,
  );

  await advanceRoundIntro();

  rerender(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={random}
      mocapInput={createMocapInput({
        latestCommand: {
          actions: ['wave_left_hand', 'wave_right_hand', 'wave'],
          hands: [{ x: 0.5, y: 0.5, state: 'open_palm', side: 'left' }],
          primaryHand: { x: 0.5, y: 0.5, state: 'open_palm', side: 'left' },
          leftHand: { x: 0.5, y: 0.5, state: 'open_palm', side: 'left' },
          rightHand: null,
        },
        rawPacketPreview: { text: 'action-only-wave', receivedAtMs: 1 },
      })}
    />,
  );

  expect(screen.queryByText('真棒')).toBeNull();
  expect(screen.queryByText('再想一想吧')).toBeNull();
  expect(
    within(screen.getByTestId('baby-object-current-item')).getByAltText('苹果'),
  ).toBeTruthy();
  vi.useRealTimers();
});

test('mocap unknown hand horizontal movement does not select a basket', async () => {
  vi.useFakeTimers();
  const random = createRandomSequence([0, 0]);
  const { rerender } = render(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={random}
      mocapInput={createMocapInput()}
    />,
  );

  await advanceRoundIntro();

  for (let index = 0; index < 4; index += 1) {
    const x = [0.22, 0.24, 0.22, 0.31][index] ?? 0.22;
    rerender(
      <BabyObjectMatchRuntimeShell
        draft={createDraft()}
        random={random}
        mocapInput={createMocapInput({
          latestCommand: {
            actions: [],
            hands: [{ x, y: 0.45, state: 'open_palm', side: 'unknown' }],
            primaryHand: { x, y: 0.45, state: 'open_palm', side: 'unknown' },
            leftHand: null,
            rightHand: null,
          },
          rawPacketPreview: {
            text: `unknown-horizontal-${index + 1}`,
            receivedAtMs: index + 1,
          },
        })}
      />,
    );
  }

  expect(screen.queryByText('真棒')).toBeNull();
  expect(screen.queryByText('再想一想吧')).toBeNull();
  expect(
    within(screen.getByTestId('baby-object-current-item')).getByAltText('苹果'),
  ).toBeTruthy();
  vi.useRealTimers();
});

test('left hand horizontal drag sends a correct item into the left basket', async () => {
  vi.useFakeTimers();
  const { container } = render(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={createRandomSequence([0, 0])}
    />,
  );
  const stage = container.querySelector('.baby-object-runtime__stage');
  if (!(stage instanceof HTMLElement)) {
    throw new Error('Missing baby object runtime stage');
  }

  await advanceRoundIntro();
  dragHand(stage, 0);

  expect(screen.getByText('真棒')).toBeTruthy();
  expect(screen.getByLabelText('成功次数').textContent).toBe('1/20');
  expect(screen.getByLabelText('左侧篮子 苹果').className).toContain(
    'baby-object-runtime__basket--correct',
  );

  await advanceFeedback();

  expect(screen.queryByText('真棒')).toBeNull();
  expect(screen.getByTestId('baby-object-current-item').textContent).toBe('');
  expect(screen.getByLabelText('礼物盒')).toBeTruthy();

  await advanceRoundIntro();

  expect(
    within(screen.getByTestId('baby-object-current-item')).getByAltText('苹果'),
  ).toBeTruthy();
  vi.useRealTimers();
});

test('ignores drag input until the item animation finishes', async () => {
  vi.useFakeTimers();
  const { container } = render(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={createRandomSequence([0, 0])}
    />,
  );
  const stage = container.querySelector('.baby-object-runtime__stage');
  if (!(stage instanceof HTMLElement)) {
    throw new Error('Missing baby object runtime stage');
  }

  dragHand(stage, 0);

  expect(screen.queryByText('真棒')).toBeNull();
  expect(screen.getByLabelText('成功次数').textContent).toBe('0/20');

  await advanceRoundIntro();
  dragHand(stage, 0);

  expect(screen.getByText('真棒')).toBeTruthy();
  expect(screen.getByLabelText('成功次数').textContent).toBe('1/20');
  vi.useRealTimers();
});

test('correct placement automatically shows the next gift item', async () => {
  vi.useFakeTimers();
  const { container } = render(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={createRandomSequence([0, 0.99])}
    />,
  );
  const stage = container.querySelector('.baby-object-runtime__stage');
  if (!(stage instanceof HTMLElement)) {
    throw new Error('Missing baby object runtime stage');
  }

  expect(screen.getByTestId('baby-object-current-item').textContent).toBe('');

  await advanceRoundIntro();

  expect(
    within(screen.getByTestId('baby-object-current-item')).getByAltText('苹果'),
  ).toBeTruthy();

  dragHand(stage, 0);

  expect(screen.getByText('真棒')).toBeTruthy();

  await advanceFeedback();

  expect(screen.queryByText('真棒')).toBeNull();
  expect(screen.getByTestId('baby-object-current-item').textContent).toBe('');

  await advanceRoundIntro();

  expect(
    within(screen.getByTestId('baby-object-current-item')).getByAltText('香蕉'),
  ).toBeTruthy();
  vi.useRealTimers();
});

test('wrong basket keeps the item active after feedback', async () => {
  vi.useFakeTimers();
  const { container } = render(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={createRandomSequence([0, 0])}
    />,
  );
  const stage = container.querySelector('.baby-object-runtime__stage');
  if (!(stage instanceof HTMLElement)) {
    throw new Error('Missing baby object runtime stage');
  }

  await advanceRoundIntro();
  dragHand(stage, 2);

  expect(screen.getByText('再想一想吧')).toBeTruthy();
  expect(screen.getByLabelText('成功次数').textContent).toBe('0/20');

  await advanceFeedback();

  expect(screen.queryByText('再想一想吧')).toBeNull();
  expect(
    within(screen.getByTestId('baby-object-current-item')).getByAltText('苹果'),
  ).toBeTruthy();
  vi.useRealTimers();
});

test('twenty correct placements completes the level', async () => {
  vi.useFakeTimers();
  const randomValues = Array.from({ length: 40 }, () => 0);
  const { container } = render(
    <BabyObjectMatchRuntimeShell
      draft={createDraft()}
      random={createRandomSequence(randomValues)}
    />,
  );
  const stage = container.querySelector('.baby-object-runtime__stage');
  if (!(stage instanceof HTMLElement)) {
    throw new Error('Missing baby object runtime stage');
  }

  for (let index = 0; index < 20; index += 1) {
    await advanceRoundIntro();
    dragHand(stage, 0);
    await advanceFeedback();
  }

  expect(screen.getAllByText('恭喜你！小朋友！').length).toBeGreaterThan(0);
  expect(screen.getByRole('button', { name: '再来一次' })).toBeTruthy();
  expect(screen.getByRole('button', { name: '下一关' })).toBeTruthy();
  vi.useRealTimers();
});
