/* @vitest-environment jsdom */

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

import type { AuthUser } from '../../services/authService';
import { AuthUiContext } from '../auth/AuthUiContext';
import {
  RpgEntryHomeView,
  type RpgEntryHomeViewProps,
} from './RpgEntryHomeView';
import type { PlatformPublicGalleryCard } from './rpgEntryWorldPresentation';

const { mockGetRpgProfileWalletLedger } = vi.hoisted(() => ({
  mockGetRpgProfileWalletLedger: vi.fn(async () => ({
    entries: [
      {
        id: 'ledger-1',
        amountDelta: -1,
        balanceAfter: 29,
        sourceType: 'asset_operation_consume',
        createdAt: '2026-04-28T10:00:00Z',
      },
      {
        id: 'ledger-2',
        amountDelta: 30,
        balanceAfter: 30,
        sourceType: 'invite_invitee_reward',
        createdAt: '2026-04-28T09:00:00Z',
      },
    ],
  })),
}));

vi.mock('../../services/rpg-entry/rpgProfileClient', () => ({
  getRpgProfileWalletLedger: mockGetRpgProfileWalletLedger,
  getRpgProfileRechargeCenter: vi.fn(async () => ({
    walletBalance: 0,
    membership: {
      status: 'normal',
      tier: 'normal',
      startedAt: null,
      expiresAt: null,
      updatedAt: null,
    },
    pointProducts: [
      {
        productId: 'points_60',
        title: '60陶泥币',
        priceCents: 600,
        kind: 'points',
        pointsAmount: 60,
        bonusPoints: 60,
        durationDays: 0,
        badgeLabel: '首充双倍',
        description: '首充送60陶泥币',
        tier: 'normal',
      },
    ],
    membershipProducts: [
      {
        productId: 'member_month',
        title: '月卡',
        priceCents: 2800,
        kind: 'membership',
        pointsAmount: 0,
        bonusPoints: 0,
        durationDays: 30,
        badgeLabel: '',
        description: '30天会员',
        tier: 'month',
      },
    ],
    benefits: [
      {
        benefitName: '免陶泥币回合数',
        normalValue: '30',
        monthValue: '100',
        seasonValue: '100',
        yearValue: '100',
      },
    ],
    latestOrder: null,
    hasPointsRecharged: false,
  })),
  createRpgProfileRechargeOrder: vi.fn(async () => ({
    order: {
      orderId: 'order-1',
      productId: 'points_60',
      productTitle: '60陶泥币',
      kind: 'points',
      amountCents: 600,
      status: 'paid',
      paymentChannel: 'mock',
      paidAt: '2026-04-25T10:00:00Z',
      createdAt: '2026-04-25T10:00:00Z',
      pointsDelta: 120,
      membershipExpiresAt: null,
    },
    center: {
      walletBalance: 120,
      membership: {
        status: 'normal',
        tier: 'normal',
        startedAt: null,
        expiresAt: null,
        updatedAt: null,
      },
      pointProducts: [],
      membershipProducts: [],
      benefits: [],
      latestOrder: null,
      hasPointsRecharged: true,
    },
  })),
}));

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

const originalMatchMedia = window.matchMedia;
const originalRequestAnimationFrame = window.requestAnimationFrame;
const originalCancelAnimationFrame = window.cancelAnimationFrame;
const puzzlePublicEntry = {
  sourceType: 'puzzle',
  workId: 'puzzle-work-public-1',
  profileId: 'puzzle-profile-public-1',
  publicWorkCode: 'PZ-EPUBLIC1',
  ownerUserId: 'user-2',
  authorDisplayName: '拼图玩家',
  worldName: '奇幻拼图',
  subtitle: '拼图关卡',
  summaryText: '一张用于公开分享的拼图作品。',
  coverImageSrc: null,
  themeTags: ['奇幻'],
  playCount: 20,
  remixCount: 5,
  likeCount: 12,
  visibility: 'published',
  publishedAt: '1777110165.990127Z',
  updatedAt: '2026-04-25T10:00:00.000Z',
} satisfies PlatformPublicGalleryCard;

const remixRankEntry = {
  ...puzzlePublicEntry,
  workId: 'puzzle-work-remix-rank',
  profileId: 'puzzle-profile-remix-rank',
  publicWorkCode: 'PZ-REMIX1',
  worldName: '改造高分拼图',
  playCount: 2,
  remixCount: 18,
  likeCount: 1,
  recentPlayCount7d: 0,
  publishedAt: '2026-04-25T11:00:00.000Z',
  updatedAt: '2026-04-25T11:00:00.000Z',
} satisfies PlatformPublicGalleryCard;

function buildCarouselPuzzleEntry(
  id: string,
  worldName: string,
  coverPrefix: string,
) {
  return {
    ...puzzlePublicEntry,
    workId: `puzzle-work-${id}`,
    profileId: `puzzle-profile-${id}`,
    publicWorkCode: `PZ-${id.toUpperCase()}`,
    worldName,
    coverImageSrc: `${coverPrefix}-fallback.png`,
    coverSlides: [
      {
        id: `${id}-cover-1`,
        imageSrc: `${coverPrefix}-1.png`,
        label: `${worldName} 1`,
      },
      {
        id: `${id}-cover-2`,
        imageSrc: `${coverPrefix}-2.png`,
        label: `${worldName} 2`,
      },
    ],
  } satisfies PlatformPublicGalleryCard;
}

const hotRankEntry = {
  ...puzzlePublicEntry,
  workId: 'puzzle-work-hot-rank',
  profileId: 'puzzle-profile-hot-rank',
  publicWorkCode: 'PZ-HOT001',
  worldName: '热门高分拼图',
  themeTags: ['奇幻', '机关'],
  playCount: 40,
  remixCount: 1,
  likeCount: 4,
  recentPlayCount7d: 0,
  publishedAt: '2026-04-24T10:00:00.000Z',
  updatedAt: '2026-04-24T10:00:00.000Z',
} satisfies PlatformPublicGalleryCard;

const newRankEntry = {
  ...puzzlePublicEntry,
  workId: 'puzzle-work-new-rank',
  profileId: 'puzzle-profile-new-rank',
  publicWorkCode: 'PZ-NEW001',
  worldName: '新品增长拼图',
  playCount: 1,
  remixCount: 0,
  likeCount: 0,
  recentPlayCount7d: 9,
  publishedAt: '2026-04-20T10:00:00.000Z',
  updatedAt: '2026-04-20T10:00:00.000Z',
} satisfies PlatformPublicGalleryCard;

const longTextRankEntry = {
  ...puzzlePublicEntry,
  workId: 'puzzle-work-long-text-rank',
  profileId: 'puzzle-profile-long-text-rank',
  publicWorkCode: 'PZ-LONG01',
  worldName: '关键词逍遥游拼图关卡',
  themeTags: ['逍遥游拼图', '古风机关'],
  playCount: 88,
  remixCount: 0,
  likeCount: 0,
  recentPlayCount7d: 0,
  publishedAt: '2026-04-29T10:00:00.000Z',
  updatedAt: '2026-04-29T10:00:00.000Z',
} satisfies PlatformPublicGalleryCard;

function mockDesktopLayout() {
  Object.defineProperty(window, 'matchMedia', {
    configurable: true,
    writable: true,
    value: vi.fn().mockImplementation(() => ({
      matches: true,
      media: '(min-width: 1024px)',
      onchange: null,
      addEventListener: vi.fn(),
      removeEventListener: vi.fn(),
      addListener: vi.fn(),
      removeListener: vi.fn(),
      dispatchEvent: vi.fn(),
    })),
  });
}

function renderProfileView(
  onRechargeSuccess = vi.fn(),
  profileDashboardOverrides: Partial<
    NonNullable<RpgEntryHomeViewProps['profileDashboard']>
  > = {},
  userOverrides: Partial<AuthUser> = {},
) {
  return render(
    <AuthUiContext.Provider
      value={{
        user: {
          id: 'user-1',
          publicUserCode: '100001',
          username: 'tester',
          displayName: '测试玩家',
          avatarUrl: null,
          phoneNumberMasked: null,
          loginMethod: 'password',
          bindingStatus: 'active',
          wechatBound: false,
          ...userOverrides,
        },
        canAccessProtectedData: true,
        openLoginModal: vi.fn(),
        requireAuth: (action) => action(),
        openSettingsModal: vi.fn(),
        openAccountModal: vi.fn(),
        setCurrentUser: vi.fn(),
        logout: vi.fn(async () => undefined),
        musicVolume: 0.42,
        setMusicVolume: vi.fn(),
        platformTheme: 'light',
        setPlatformTheme: vi.fn(),
        isHydratingSettings: false,
        isPersistingSettings: false,
        settingsError: null,
      }}
    >
      <RpgEntryHomeView
        activeTab="profile"
        onTabChange={vi.fn()}
        hasSavedGame={false}
        savedSnapshot={null}
        saveEntries={[]}
        saveError={null}
        featuredEntries={[]}
        latestEntries={[]}
        myEntries={[]}
        historyEntries={[]}
        profileDashboard={{
          walletBalance: 0,
          totalPlayTimeMs: 0,
          playedWorldCount: 0,
          updatedAt: null,
          ...profileDashboardOverrides,
        }}
        isLoadingPlatform={false}
        isLoadingDashboard={false}
        isResumingSaveWorldKey={null}
        platformError={null}
        dashboardError={null}
        onContinueGame={vi.fn()}
        onResumeSave={vi.fn()}
        onOpenCreateWorld={vi.fn()}
        onOpenCreateTypePicker={vi.fn()}
        onOpenGalleryDetail={vi.fn()}
        onOpenLibraryDetail={vi.fn()}
        onSearchPublicCode={vi.fn()}
        onRechargeSuccess={onRechargeSuccess}
      />
    </AuthUiContext.Provider>,
  );
}

function renderLoggedOutHomeView(
  openLoginModal = vi.fn(),
  overrides: Partial<
    Pick<
      RpgEntryHomeViewProps,
      | 'featuredEntries'
      | 'latestEntries'
      | 'onOpenGalleryDetail'
      | 'onSearchPublicCode'
    >
  > = {},
) {
  return render(
    <AuthUiContext.Provider
      value={{
        user: null,
        canAccessProtectedData: false,
        openLoginModal,
        requireAuth: vi.fn(),
        openSettingsModal: vi.fn(),
        openAccountModal: vi.fn(),
        setCurrentUser: vi.fn(),
        logout: vi.fn(async () => undefined),
        musicVolume: 0.42,
        setMusicVolume: vi.fn(),
        platformTheme: 'light',
        setPlatformTheme: vi.fn(),
        isHydratingSettings: false,
        isPersistingSettings: false,
        settingsError: null,
      }}
    >
      <RpgEntryHomeView
        activeTab="home"
        onTabChange={vi.fn()}
        hasSavedGame={false}
        savedSnapshot={null}
        saveEntries={[]}
        saveError={null}
        featuredEntries={overrides.featuredEntries ?? []}
        latestEntries={overrides.latestEntries ?? []}
        myEntries={[]}
        historyEntries={[]}
        profileDashboard={null}
        isLoadingPlatform={false}
        isLoadingDashboard={false}
        isResumingSaveWorldKey={null}
        platformError={null}
        dashboardError={null}
        onContinueGame={vi.fn()}
        onResumeSave={vi.fn()}
        onOpenCreateWorld={vi.fn()}
        onOpenCreateTypePicker={vi.fn()}
        onOpenGalleryDetail={overrides.onOpenGalleryDetail ?? vi.fn()}
        onOpenLibraryDetail={vi.fn()}
        onSearchPublicCode={overrides.onSearchPublicCode ?? vi.fn()}
      />
    </AuthUiContext.Provider>,
  );
}

function renderStatefulLoggedOutHomeView(
  overrides: Partial<
    Pick<RpgEntryHomeViewProps, 'featuredEntries' | 'latestEntries'>
  > = {},
) {
  function StatefulLoggedOutHomeView() {
    const [activeTab, setActiveTab] =
      useState<RpgEntryHomeViewProps['activeTab']>('home');

    return (
      <AuthUiContext.Provider
        value={{
          user: null,
          canAccessProtectedData: false,
          openLoginModal: vi.fn(),
          requireAuth: vi.fn(),
          openSettingsModal: vi.fn(),
          openAccountModal: vi.fn(),
          setCurrentUser: vi.fn(),
          logout: vi.fn(async () => undefined),
          musicVolume: 0.42,
          setMusicVolume: vi.fn(),
          platformTheme: 'light',
          setPlatformTheme: vi.fn(),
          isHydratingSettings: false,
          isPersistingSettings: false,
          settingsError: null,
        }}
      >
        <RpgEntryHomeView
          activeTab={activeTab}
          onTabChange={setActiveTab}
          hasSavedGame={false}
          savedSnapshot={null}
          saveEntries={[]}
          saveError={null}
          featuredEntries={overrides.featuredEntries ?? []}
          latestEntries={overrides.latestEntries ?? []}
          myEntries={[]}
          historyEntries={[]}
          profileDashboard={null}
          isLoadingPlatform={false}
          isLoadingDashboard={false}
          isResumingSaveWorldKey={null}
          platformError={null}
          dashboardError={null}
          onContinueGame={vi.fn()}
          onResumeSave={vi.fn()}
          onOpenCreateWorld={vi.fn()}
          onOpenCreateTypePicker={vi.fn()}
          onOpenGalleryDetail={vi.fn()}
          onOpenLibraryDetail={vi.fn()}
          onSearchPublicCode={vi.fn()}
        />
      </AuthUiContext.Provider>
    );
  }

  return render(<StatefulLoggedOutHomeView />);
}

afterEach(() => {
  vi.useRealTimers();
  vi.clearAllMocks();
  Object.defineProperty(window, 'matchMedia', {
    configurable: true,
    writable: true,
    value: originalMatchMedia,
  });
  Object.defineProperty(window, 'requestAnimationFrame', {
    configurable: true,
    writable: true,
    value: originalRequestAnimationFrame,
  });
  Object.defineProperty(window, 'cancelAnimationFrame', {
    configurable: true,
    writable: true,
    value: originalCancelAnimationFrame,
  });
  Object.defineProperty(navigator, 'clipboard', {
    configurable: true,
    value: undefined,
  });
});

test('opens wallet ledger modal from narrative coin card', async () => {
  const user = userEvent.setup();

  renderProfileView();
  await user.click(screen.getByRole('button', { name: /陶泥币\s*0/u }));

  expect(await screen.findByText('陶泥币账单')).toBeTruthy();
  expect(mockGetRpgProfileWalletLedger).toHaveBeenCalledTimes(1);
  expect(screen.getByText('资产操作消耗')).toBeTruthy();
  expect(screen.getByText('-1')).toBeTruthy();
  expect(screen.getByText('填写邀请码奖励')).toBeTruthy();
  expect(screen.getByText('+30')).toBeTruthy();
});

test('profile total play time card always uses hours', () => {
  renderProfileView(vi.fn(), {
    totalPlayTimeMs: 90 * 60 * 1000,
  });

  const playTimeCard = screen.getByRole('button', {
    name: /游戏时长/u,
  });

  expect(within(playTimeCard).getByText('1.5小时')).toBeTruthy();
  expect(within(playTimeCard).queryByText('90分')).toBeNull();
});

test('profile played works card shows count unit', () => {
  renderProfileView(vi.fn(), {
    playedWorldCount: 1,
  });

  const playedCard = screen.getByRole('button', {
    name: /玩过\s*1个/u,
  });

  expect(within(playedCard).getByText('1个')).toBeTruthy();
});

test('desktop account entry uses saved avatar image when available', () => {
  mockDesktopLayout();
  const avatarUrl = 'data:image/png;base64,AAAA';

  renderProfileView(vi.fn(), {}, { avatarUrl });

  const accountEntry = screen.getByRole('button', { name: /测试玩家/u });
  const avatarImage = accountEntry.querySelector('img');
  expect(avatarImage?.getAttribute('src')).toBe(avatarUrl);
  expect(within(accountEntry).queryByText('测')).toBeNull();
});

test('wallet ledger modal shows empty and error states', async () => {
  const user = userEvent.setup();
  mockGetRpgProfileWalletLedger.mockResolvedValueOnce({ entries: [] });

  renderProfileView();
  await user.click(screen.getByRole('button', { name: /陶泥币\s*0/u }));
  expect(await screen.findByText('暂无账单记录')).toBeTruthy();

  await user.click(screen.getByLabelText('关闭陶泥币账单'));
  mockGetRpgProfileWalletLedger.mockRejectedValueOnce(new Error('加载失败'));
  await user.click(screen.getByRole('button', { name: /陶泥币\s*0/u }));

  expect(await screen.findByText('加载失败')).toBeTruthy();
  expect(screen.getByText('重新加载')).toBeTruthy();
});

test('opens reward code modal from profile action on mobile', async () => {
  const user = userEvent.setup();

  renderProfileView();
  await user.click(screen.getByRole('button', { name: /兑换码/u }));

  const modal = await screen.findByPlaceholderText('输入兑换码');
  expect(modal).toBeTruthy();
  expect(screen.getByRole('button', { name: '兑换' })).toBeTruthy();
  expect(screen.getByLabelText('关闭兑换码')).toBeTruthy();
});

test('shows a reachable login entry in logged out mobile shell', async () => {
  const user = userEvent.setup();
  const openLoginModal = vi.fn();

  renderLoggedOutHomeView(openLoginModal);
  await user.click(screen.getByRole('button', { name: '登录' }));

  expect(openLoginModal).toHaveBeenCalledTimes(1);
});

test('mobile home search submits public work code', async () => {
  const user = userEvent.setup();
  const onSearchPublicCode = vi.fn();

  render(
    <AuthUiContext.Provider
      value={{
        user: null,
        canAccessProtectedData: false,
        openLoginModal: vi.fn(),
        requireAuth: vi.fn(),
        openSettingsModal: vi.fn(),
        openAccountModal: vi.fn(),
        setCurrentUser: vi.fn(),
        logout: vi.fn(async () => undefined),
        musicVolume: 0.42,
        setMusicVolume: vi.fn(),
        platformTheme: 'light',
        setPlatformTheme: vi.fn(),
        isHydratingSettings: false,
        isPersistingSettings: false,
        settingsError: null,
      }}
    >
      <RpgEntryHomeView
        activeTab="home"
        onTabChange={vi.fn()}
        hasSavedGame={false}
        savedSnapshot={null}
        saveEntries={[]}
        saveError={null}
        featuredEntries={[]}
        latestEntries={[]}
        myEntries={[]}
        historyEntries={[]}
        profileDashboard={null}
        isLoadingPlatform={false}
        isLoadingDashboard={false}
        isResumingSaveWorldKey={null}
        platformError={null}
        dashboardError={null}
        onContinueGame={vi.fn()}
        onResumeSave={vi.fn()}
        onOpenCreateWorld={vi.fn()}
        onOpenCreateTypePicker={vi.fn()}
        onOpenGalleryDetail={vi.fn()}
        onOpenLibraryDetail={vi.fn()}
        onSearchPublicCode={onSearchPublicCode}
      />
    </AuthUiContext.Provider>,
  );

  const searchInput = screen.getByPlaceholderText(
    '输入 SY / CW / BF / M3 / PZ 编号',
  );
  await user.type(searchInput, 'PZ-PROFILE1{enter}');

  expect(onSearchPublicCode).toHaveBeenCalledWith('PZ-PROFILE1');
});

test('public gallery cards hide work code until detail is opened', async () => {
  const user = userEvent.setup();
  const onOpenGalleryDetail = vi.fn();

  renderLoggedOutHomeView(vi.fn(), {
    latestEntries: [puzzlePublicEntry],
    onOpenGalleryDetail,
  });

  expect(screen.queryByText('PZ-EPUBLIC1')).toBeNull();
  expect(
    screen.queryByRole('button', { name: '复制作品号 PZ-EPUBLIC1' }),
  ).toBeNull();

  await user.click(screen.getByRole('button', { name: /奇幻拼图/u }));

  expect(onOpenGalleryDetail).toHaveBeenCalledWith(puzzlePublicEntry);
});

test('mobile public work cards render cover, author, kind and cover stats', () => {
  const { container } = renderLoggedOutHomeView(vi.fn(), {
    latestEntries: [puzzlePublicEntry],
  });

  const card = screen.getByRole('button', {
    name: /奇幻拼图，拼图，20游玩，5改造，12点赞/u,
  });
  expect(
    card.querySelector('.platform-public-work-card__cover.aspect-video'),
  ).toBeTruthy();
  expect(
    card.querySelector('.platform-public-work-card__cover-stats'),
  ).toBeTruthy();
  expect(
    card.querySelectorAll('.platform-public-work-card__cover-stat'),
  ).toHaveLength(3);
  expect(
    card.querySelector('.platform-public-work-card__kind')?.textContent,
  ).toBe('拼图');
  expect(
    card.querySelector('.platform-public-work-card__author-avatar')
      ?.textContent,
  ).toBe('拼');
  expect(screen.getByText('奇幻拼图')).toBeTruthy();
  expect(screen.getByText('拼图玩家')).toBeTruthy();
  expect(screen.getByText('一张用于公开分享的拼图作品。')).toBeTruthy();
  expect(screen.getByText('奇幻')).toBeTruthy();
  expect(screen.getByText('20')).toBeTruthy();
  expect(screen.getByText('5')).toBeTruthy();
  expect(screen.getByText('12')).toBeTruthy();
  expect(card.querySelector('.platform-pill--warm')?.textContent).not.toBe(
    '推荐',
  );
  expect(
    container.querySelector('.platform-mobile-home-channel--active')
      ?.textContent,
  ).toBe('推荐');
});

test('mobile home feed only rotates the card closest to screen center', () => {
  vi.useFakeTimers();
  Object.defineProperty(window, 'requestAnimationFrame', {
    configurable: true,
    writable: true,
    value: (callback: FrameRequestCallback) =>
      window.setTimeout(() => callback(0), 0),
  });
  Object.defineProperty(window, 'cancelAnimationFrame', {
    configurable: true,
    writable: true,
    value: (handle: number) => window.clearTimeout(handle),
  });

  const firstEntry = buildCarouselPuzzleEntry('center1', '中心拼图一', 'center-one');
  const secondEntry = buildCarouselPuzzleEntry(
    'center2',
    '中心拼图二',
    'center-two',
  );
  const cardRects = new Map<string, DOMRect>();

  renderLoggedOutHomeView(vi.fn(), {
    latestEntries: [firstEntry, secondEntry],
  });

  const tabPanel = document.querySelector('.platform-tab-panel--active');
  const firstCard = screen.getByRole('button', { name: /中心拼图一/u });
  const secondCard = screen.getByRole('button', { name: /中心拼图二/u });
  if (!tabPanel) {
    throw new Error('缺少移动端首页滚动面板');
  }

  tabPanel.getBoundingClientRect = vi.fn(
    () =>
      ({
        top: 0,
        bottom: 600,
        height: 600,
        left: 0,
        right: 360,
        width: 360,
      }) as DOMRect,
  );
  firstCard.getBoundingClientRect = vi.fn(() => cardRects.get('first')!);
  secondCard.getBoundingClientRect = vi.fn(() => cardRects.get('second')!);
  cardRects.set('first', {
    top: 170,
    bottom: 370,
    height: 200,
    left: 0,
    right: 320,
    width: 320,
  } as DOMRect);
  cardRects.set('second', {
    top: 420,
    bottom: 620,
    height: 200,
    left: 0,
    right: 320,
    width: 320,
  } as DOMRect);

  act(() => {
    vi.runOnlyPendingTimers();
  });

  expect(within(firstCard).getByRole('img').getAttribute('src')).toBe(
    'center-one-1.png',
  );
  expect(within(secondCard).getByRole('img').getAttribute('src')).toBe(
    'center-two-1.png',
  );

  act(() => {
    vi.advanceTimersByTime(4200);
  });

  expect(within(firstCard).getByRole('img').getAttribute('src')).toBe(
    'center-one-2.png',
  );
  expect(within(secondCard).getByRole('img').getAttribute('src')).toBe(
    'center-two-1.png',
  );

  cardRects.set('first', {
    top: -120,
    bottom: 80,
    height: 200,
    left: 0,
    right: 320,
    width: 320,
  } as DOMRect);
  cardRects.set('second', {
    top: 200,
    bottom: 400,
    height: 200,
    left: 0,
    right: 320,
    width: 320,
  } as DOMRect);

  act(() => {
    tabPanel.dispatchEvent(new Event('scroll'));
    vi.runOnlyPendingTimers();
  });

  expect(within(firstCard).getByRole('img').getAttribute('src')).toBe(
    'center-one-1.png',
  );

  act(() => {
    vi.advanceTimersByTime(4200);
  });

  expect(within(secondCard).getByRole('img').getAttribute('src')).toBe(
    'center-two-2.png',
  );
});

test('mobile today channel only shows newly published works from today', async () => {
  const user = userEvent.setup();
  const now = new Date();
  const todayPublishedAt = new Date(
    now.getFullYear(),
    now.getMonth(),
    now.getDate(),
    10,
  ).toISOString();
  const yesterdayPublishedAt = new Date(
    now.getFullYear(),
    now.getMonth(),
    now.getDate() - 1,
    10,
  ).toISOString();
  const todayEntry = {
    ...puzzlePublicEntry,
    workId: 'puzzle-work-today',
    profileId: 'puzzle-profile-today',
    publicWorkCode: 'PZ-TODAY1',
    worldName: '今日新游',
    publishedAt: todayPublishedAt,
    updatedAt: todayPublishedAt,
  } satisfies PlatformPublicGalleryCard;
  const yesterdayEntry = {
    ...puzzlePublicEntry,
    workId: 'puzzle-work-yesterday',
    profileId: 'puzzle-profile-yesterday',
    publicWorkCode: 'PZ-YDAY01',
    worldName: '昨日旧作',
    publishedAt: yesterdayPublishedAt,
    updatedAt: yesterdayPublishedAt,
  } satisfies PlatformPublicGalleryCard;
  const updatedTodayEntry = {
    ...puzzlePublicEntry,
    workId: 'puzzle-work-updated-today',
    profileId: 'puzzle-profile-updated-today',
    publicWorkCode: 'PZ-UPDAY1',
    worldName: '今日更新旧作',
    publishedAt: yesterdayPublishedAt,
    updatedAt: todayPublishedAt,
  } satisfies PlatformPublicGalleryCard;

  renderLoggedOutHomeView(vi.fn(), {
    latestEntries: [yesterdayEntry, updatedTodayEntry, todayEntry],
  });

  await user.click(screen.getByRole('button', { name: '今日游戏' }));

  expect(screen.getByRole('button', { name: /今日新游/u })).toBeTruthy();
  expect(screen.queryByText('昨日旧作')).toBeNull();
  expect(screen.queryByText('今日更新旧作')).toBeNull();
});

test('desktop home syncs mobile home modules without square or latest labels', () => {
  mockDesktopLayout();
  const todayPublishedAt = new Date().toISOString();
  const todayEntry = {
    ...puzzlePublicEntry,
    workId: 'puzzle-work-desktop-today',
    profileId: 'puzzle-profile-desktop-today',
    publicWorkCode: 'PZ-DTODAY',
    worldName: '桌面今日新游',
    publishedAt: todayPublishedAt,
    updatedAt: todayPublishedAt,
  } satisfies PlatformPublicGalleryCard;

  renderLoggedOutHomeView(vi.fn(), {
    latestEntries: [puzzlePublicEntry, todayEntry],
  });

  expect(screen.getByText('今日游戏')).toBeTruthy();
  expect(screen.getByText('推荐')).toBeTruthy();
  expect(screen.getByText('作品分类')).toBeTruthy();
  expect(screen.getAllByText('桌面今日新游').length).toBeGreaterThan(0);
  expect(screen.queryByText('趋势关注')).toBeNull();
  expect(screen.queryByText('最新发布')).toBeNull();
  expect(screen.queryByText('作品广场')).toBeNull();
  expect(screen.queryByText('公开作品')).toBeNull();
  expect(screen.queryByText('PZ-EPUBLIC1')).toBeNull();
  expect(screen.getAllByText('拼图').length).toBeGreaterThan(0);
  expect(screen.queryByText('1777110165.990127Z')).toBeNull();
});

test('mobile home moves category shelf into game category channel', async () => {
  const user = userEvent.setup();
  const { container } = renderLoggedOutHomeView(vi.fn(), {
    latestEntries: [puzzlePublicEntry],
  });

  expect(screen.queryByRole('button', { name: 'PC游戏' })).toBeNull();
  expect(screen.queryByRole('button', { name: '即点即玩' })).toBeNull();

  await user.click(screen.getByRole('button', { name: '游戏分类' }));

  expect(screen.getAllByText('游戏分类').length).toBeGreaterThan(0);
  expect(screen.getByRole('button', { name: /筛选/u })).toBeTruthy();
  expect(screen.getByRole('button', { name: '奇幻' })).toBeTruthy();
  expect(screen.getByRole('button', { name: /奇幻拼图，试玩/u })).toBeTruthy();
  expect(container.querySelector('.platform-category-game-list')).toBeTruthy();
  expect(container.querySelector('.platform-category-game-item')).toBeTruthy();
  expect(
    container.querySelector('.platform-category-game-item__action')
      ?.textContent,
  ).toBe('试玩');
});

test('mobile game category list orders works by composite public metric', async () => {
  const user = userEvent.setup();

  renderLoggedOutHomeView(vi.fn(), {
    latestEntries: [puzzlePublicEntry, hotRankEntry],
  });

  await user.click(screen.getByRole('button', { name: '游戏分类' }));
  await user.click(screen.getByRole('button', { name: '奇幻' }));

  const gameItems = Array.from(
    document.querySelectorAll('.platform-category-game-item__title'),
  ).map((element) => element.textContent);
  expect(gameItems).toEqual(['热门高分拼图', '奇幻拼图']);
});

test('bottom category tab becomes ranking and switches ranking metrics', async () => {
  const user = userEvent.setup();

  renderStatefulLoggedOutHomeView({
    latestEntries: [remixRankEntry, hotRankEntry, newRankEntry],
  });

  expect(screen.queryByRole('button', { name: '分类' })).toBeNull();

  await user.click(screen.getByRole('button', { name: '排行' }));

  expect(await screen.findByRole('tab', { name: '热门榜' })).toBeTruthy();
  expect(screen.getByRole('tab', { name: '改造榜' })).toBeTruthy();
  expect(screen.getByRole('tab', { name: '新品榜' })).toBeTruthy();
  expect(screen.getByRole('tab', { name: '点赞榜' })).toBeTruthy();

  const rankingPanel = document.getElementById('platform-tab-panel-category');
  expect(rankingPanel?.getAttribute('aria-hidden')).toBe('false');
  expect(within(rankingPanel!).getByText('热门高分拼图')).toBeTruthy();
  expect(within(rankingPanel!).getByText('40')).toBeTruthy();
  expect(within(rankingPanel!).getAllByText('游玩').length).toBeGreaterThan(0);

  await user.click(screen.getByRole('tab', { name: '改造榜' }));

  expect(within(rankingPanel!).getByText('改造高分拼图')).toBeTruthy();
  expect(within(rankingPanel!).getByText('18')).toBeTruthy();
  expect(within(rankingPanel!).getAllByText('改造').length).toBeGreaterThan(0);

  await user.click(screen.getByRole('tab', { name: '新品榜' }));

  expect(within(rankingPanel!).getByText('新品增长拼图')).toBeTruthy();
  expect(within(rankingPanel!).getByText('9')).toBeTruthy();
  expect(within(rankingPanel!).getAllByText('近7日').length).toBeGreaterThan(0);
});

test('ranking rows limit displayed work name and show two short tags on the third line', async () => {
  const user = userEvent.setup();

  renderStatefulLoggedOutHomeView({
    latestEntries: [longTextRankEntry],
  });

  await user.click(screen.getByRole('button', { name: '排行' }));

  const rankingPanel = document.getElementById('platform-tab-panel-category');
  expect(rankingPanel).toBeTruthy();
  expect(within(rankingPanel!).getByText('关键词逍遥游拼图')).toBeTruthy();
  expect(within(rankingPanel!).queryByText('关键词逍遥游拼图关卡')).toBeNull();
  expect(within(rankingPanel!).getByText('逍遥游拼')).toBeTruthy();
  expect(within(rankingPanel!).getByText('古风机关')).toBeTruthy();
  expect(within(rankingPanel!).queryByText(/2026-04-29/u)).toBeNull();
  expect(within(rankingPanel!).queryByText('拼图玩家')).toBeNull();
});
