/* @vitest-environment jsdom */

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

import type { AuthSessionSummary, AuthUser } from '../../services/authService';
import { LEGAL_CONSENT_STORAGE_KEY } from '../common/legalDocuments';
import { AuthGate, setAuthGateReloadForTest } from './AuthGate';
import { useAuthUi } from './AuthUiContext';

const authMocks = vi.hoisted(() => ({
  authEntry: vi.fn(),
  changePassword: vi.fn(),
  ensureStoredAccessToken: vi.fn(),
  getStoredAccessToken: vi.fn(),
  refreshStoredAccessToken: vi.fn(),
  getAuthLoginOptions: vi.fn(),
  getCurrentAuthUser: vi.fn(),
  loginWithPhoneCode: vi.fn(),
  logoutAllAuthSessions: vi.fn(),
  logoutAuthUser: vi.fn(),
  redeemRegistrationInviteCode: vi.fn(),
  resetPassword: vi.fn(),
  getAuthAuditLogs: vi.fn(),
  getAuthRiskBlocks: vi.fn(),
  getAuthSessions: vi.fn(),
  isWechatMiniProgramWebViewRuntime: vi.fn(() => false),
  requestWechatMiniProgramPhoneLogin: vi.fn(),
  revokeAuthSessions: vi.fn(),
  sendPhoneLoginCode: vi.fn(),
  startWechatLogin: vi.fn(),
  consumeAuthCallbackResult: vi.fn(),
}));

vi.mock('../../services/apiClient', async () => {
  const actual =
    await vi.importActual<typeof import('../../services/apiClient')>(
      '../../services/apiClient',
    );

  return {
    ...actual,
    AUTH_STATE_EVENT: 'genarrative-auth-state-changed',
    ensureStoredAccessToken: authMocks.ensureStoredAccessToken,
    getStoredAccessToken: authMocks.getStoredAccessToken,
    refreshStoredAccessToken: authMocks.refreshStoredAccessToken,
  };
});

vi.mock('../../services/authService', () => ({
  authEntry: authMocks.authEntry,
  bindWechatPhone: vi.fn(),
  changePassword: authMocks.changePassword,
  changePhoneNumber: vi.fn(),
  consumeAuthCallbackResult: authMocks.consumeAuthCallbackResult,
  getStoredLastLoginPhone: vi.fn(() => ''),
  getAuthAuditLogs: authMocks.getAuthAuditLogs,
  getAuthLoginOptions: authMocks.getAuthLoginOptions,
  getAuthRiskBlocks: authMocks.getAuthRiskBlocks,
  getCurrentAuthUser: authMocks.getCurrentAuthUser,
  getAuthSessions: authMocks.getAuthSessions,
  getCaptchaChallengeFromError: vi.fn(() => null),
  isWechatMiniProgramWebViewRuntime:
    authMocks.isWechatMiniProgramWebViewRuntime,
  liftAuthRiskBlock: vi.fn(),
  loginWithPhoneCode: authMocks.loginWithPhoneCode,
  logoutAllAuthSessions: authMocks.logoutAllAuthSessions,
  logoutAuthUser: authMocks.logoutAuthUser,
  requestWechatMiniProgramPhoneLogin:
    authMocks.requestWechatMiniProgramPhoneLogin,
  redeemRegistrationInviteCode: authMocks.redeemRegistrationInviteCode,
  resetPassword: authMocks.resetPassword,
  revokeAuthSessions: authMocks.revokeAuthSessions,
  sendPhoneLoginCode: authMocks.sendPhoneLoginCode,
  setStoredLastLoginPhone: vi.fn(),
  startWechatLogin: authMocks.startWechatLogin,
}));

vi.mock('../../hooks/useGameSettings', () => ({
  useGameSettings: () => ({
    musicVolume: 0.42,
    setMusicVolume: () => {},
    platformTheme: 'light',
    setPlatformTheme: () => {},
    hasHydratedSettings: true,
    isHydratingSettings: false,
    isPersistingSettings: false,
    settingsError: null,
  }),
}));

vi.mock('./AccountModal', async () => {
  const actual =
    await vi.importActual<typeof import('./AccountModal')>('./AccountModal');

  return actual;
});

vi.mock('./BindPhoneScreen', () => ({
  BindPhoneScreen: () => <div>绑定手机号</div>,
}));

const mockUser: AuthUser = {
  id: 'user-1',
  displayName: '测试玩家',
  avatarUrl: null,
  publicUserCode: 'user-tester',
  phoneNumberMasked: '138****8000',
  loginMethod: 'phone',
  bindingStatus: 'active',
  wechatBound: false,
};

beforeEach(() => {
  vi.clearAllMocks();
  window.localStorage.clear();
  window.history.replaceState(null, '', '/');
  setAuthGateReloadForTest(vi.fn());
  authMocks.consumeAuthCallbackResult.mockReturnValue(null);
  authMocks.ensureStoredAccessToken.mockResolvedValue('jwt-existing-token');
  authMocks.getStoredAccessToken.mockReturnValue('');
  authMocks.refreshStoredAccessToken.mockResolvedValue('jwt-refreshed-token');
  authMocks.getCurrentAuthUser.mockResolvedValue({
    user: null,
    availableLoginMethods: ['phone'],
  });
  authMocks.loginWithPhoneCode.mockResolvedValue({
    token: 'jwt-phone',
    user: mockUser,
    created: false,
    referral: null,
  });
  authMocks.authEntry.mockResolvedValue(mockUser);
  authMocks.changePassword.mockResolvedValue(mockUser);
  authMocks.logoutAllAuthSessions.mockResolvedValue(undefined);
  authMocks.logoutAuthUser.mockResolvedValue(undefined);
  authMocks.getAuthAuditLogs.mockResolvedValue([]);
  authMocks.getAuthRiskBlocks.mockResolvedValue([]);
  authMocks.getAuthSessions.mockResolvedValue([]);
  authMocks.revokeAuthSessions.mockResolvedValue(undefined);
  authMocks.redeemRegistrationInviteCode.mockResolvedValue({
    center: {
      inviteCode: 'SY12345678',
      inviteLinkPath: '/?inviteCode=SY12345678',
      invitedCount: 1,
      rewardedInviteCount: 1,
      todayInviterRewardCount: 0,
      todayInviterRewardRemaining: 3,
      rewardPoints: 30,
      hasRedeemedCode: true,
      boundInviterUserId: 'user_inviter',
      boundAt: '2026-05-01T00:00:00Z',
      updatedAt: '2026-05-01T00:00:00Z',
    },
    inviteeRewardGranted: true,
    inviterRewardGranted: true,
    inviteeBalanceAfter: 30,
    inviterBalanceAfter: 30,
  });
  authMocks.resetPassword.mockResolvedValue(mockUser);
  authMocks.sendPhoneLoginCode.mockResolvedValue({
    cooldownSeconds: 60,
    expiresInSeconds: 300,
  });
  authMocks.startWechatLogin.mockResolvedValue(undefined);
  authMocks.isWechatMiniProgramWebViewRuntime.mockReturnValue(false);
  authMocks.requestWechatMiniProgramPhoneLogin.mockResolvedValue(true);
});

afterEach(() => {
  setAuthGateReloadForTest(null);
});

async function acceptLegalConsent(
  user: ReturnType<typeof userEvent.setup>,
  dialog: HTMLElement,
) {
  await user.click(
    within(dialog).getByRole('switch', { name: '同意法律协议' }),
  );
}

function ProtectedActionButton({
  onAuthenticated,
}: {
  onAuthenticated: () => void;
}) {
  const authUi = useAuthUi();

  return (
    <button
      type="button"
      onClick={() => {
        authUi?.requireAuth(onAuthenticated);
      }}
    >
      进入作品
    </button>
  );
}

function PlatformTabStateProbe() {
  const [tab, setTab] = useState<'home' | 'create'>('home');

  return (
    <div>
      <div>当前Tab：{tab === 'home' ? '首页' : '创作'}</div>
      <button type="button" onClick={() => setTab('create')}>
        创作
      </button>
    </div>
  );
}

function LogoutStateProbe() {
  const authUi = useAuthUi();

  return (
    <div>
      <div>当前用户：{authUi?.user?.displayName ?? '未登录'}</div>
      <div>
        私有数据：{authUi?.canAccessProtectedData ? '可读取' : '不可读取'}
      </div>
      <button
        type="button"
        onClick={() => {
          void authUi?.logout();
        }}
      >
        退出登录
      </button>
    </div>
  );
}

function AccountPanelProbe() {
  const authUi = useAuthUi();

  return (
    <button
      type="button"
      onClick={() => {
        authUi?.openAccountModal();
      }}
    >
      打开账号面板
    </button>
  );
}

test('auth gate keeps platform content visible when phone login is available', async () => {
  authMocks.getAuthLoginOptions.mockResolvedValue({
    availableLoginMethods: ['phone'],
  });

  render(
    <AuthGate>
      <div>应用内容</div>
    </AuthGate>,
  );

  expect(await screen.findByText('应用内容')).toBeTruthy();
  expect(screen.queryByRole('button', { name: '登录' })).toBeNull();
  expect(screen.queryByText('先登录账号，再同步你的冒险进度。')).toBeNull();
});

test('auth gate waits for refresh cookie rotation before exposing restored user content', async () => {
  let resolveToken!: (token: string) => void;
  const tokenPromise = new Promise<string>((resolve) => {
    resolveToken = resolve;
  });
  authMocks.refreshStoredAccessToken.mockReturnValue(tokenPromise);
  authMocks.getCurrentAuthUser.mockResolvedValue({
    user: mockUser,
    availableLoginMethods: ['phone'],
  });

  render(
    <AuthGate>
      <div>应用内容</div>
    </AuthGate>,
  );

  expect(screen.getByText('正在校验登录状态...')).toBeTruthy();
  expect(authMocks.getCurrentAuthUser).not.toHaveBeenCalled();

  resolveToken('jwt-refreshed-token');

  expect(await screen.findByText('应用内容')).toBeTruthy();
  expect(authMocks.refreshStoredAccessToken).toHaveBeenCalledTimes(1);
  expect(authMocks.refreshStoredAccessToken).toHaveBeenCalledWith({
    clearOnFailure: true,
  });
  expect(authMocks.ensureStoredAccessToken).not.toHaveBeenCalled();
  expect(authMocks.getCurrentAuthUser).toHaveBeenCalledTimes(1);
});

test('auth gate keeps a valid local token login when refresh rotation fails after reload', async () => {
  authMocks.getStoredAccessToken.mockReturnValue('jwt-existing-token');
  authMocks.refreshStoredAccessToken.mockRejectedValue(
    new Error('refresh cookie 失效'),
  );
  authMocks.getCurrentAuthUser.mockResolvedValue({
    user: mockUser,
    availableLoginMethods: ['phone'],
  });

  render(
    <AuthGate>
      <LogoutStateProbe />
    </AuthGate>,
  );

  expect(await screen.findByText('当前用户：测试玩家')).toBeTruthy();
  expect(screen.getByText('私有数据：可读取')).toBeTruthy();
  expect(authMocks.refreshStoredAccessToken).toHaveBeenCalledWith({
    clearOnFailure: false,
  });
  expect(authMocks.getCurrentAuthUser).toHaveBeenCalledTimes(1);
});

test('auth gate does not auto-create a guest account when dev guest switch is not explicitly enabled', async () => {
  authMocks.getAuthLoginOptions.mockResolvedValue({
    availableLoginMethods: [],
  });

  render(
    <AuthGate>
      <div>应用内容</div>
    </AuthGate>,
  );

  expect(await screen.findByText('应用内容')).toBeTruthy();
});

test('auth gate keeps sms and password entries available when login options are empty', async () => {
  const user = userEvent.setup();

  authMocks.getCurrentAuthUser.mockResolvedValue({
    user: null,
    availableLoginMethods: [],
  });
  authMocks.getAuthLoginOptions.mockResolvedValue({
    availableLoginMethods: [],
  });

  render(
    <AuthGate>
      <ProtectedActionButton onAuthenticated={vi.fn()} />
    </AuthGate>,
  );

  await user.click(await screen.findByRole('button', { name: '进入作品' }));

  const dialog = screen.getByRole('dialog', { name: '账号入口' });
  expect(within(dialog).getByRole('tab', { name: '短信登录' })).toBeTruthy();
  expect(within(dialog).getByRole('tab', { name: '密码登录' })).toBeTruthy();
  expect(within(dialog).getByLabelText('验证码')).toBeTruthy();
  expect(
    within(dialog).getByRole('button', { name: '获取验证码' }),
  ).toBeTruthy();
  await user.click(within(dialog).getByRole('tab', { name: '密码登录' }));
  expect(within(dialog).getByLabelText('密码')).toBeTruthy();
  expect(within(dialog).queryByText('当前登录入口暂不可用。')).toBeNull();
  expect(within(dialog).queryByText('读取登录方式失败')).toBeNull();
});

test('auth gate keeps sms and password entries available when login options request fails', async () => {
  const user = userEvent.setup();

  authMocks.getAuthLoginOptions.mockRejectedValue(
    new Error('读取登录方式失败'),
  );

  render(
    <AuthGate>
      <ProtectedActionButton onAuthenticated={vi.fn()} />
    </AuthGate>,
  );

  await user.click(await screen.findByRole('button', { name: '进入作品' }));

  const dialog = screen.getByRole('dialog', { name: '账号入口' });
  expect(within(dialog).getByRole('tab', { name: '短信登录' })).toBeTruthy();
  expect(within(dialog).getByRole('tab', { name: '密码登录' })).toBeTruthy();
  expect(within(dialog).getByLabelText('验证码')).toBeTruthy();
  expect(
    within(dialog).getByRole('button', { name: '获取验证码' }),
  ).toBeTruthy();
  await user.click(within(dialog).getByRole('tab', { name: '密码登录' }));
  expect(within(dialog).getByLabelText('密码')).toBeTruthy();
  expect(within(dialog).queryByText('当前登录入口暂不可用。')).toBeNull();
});

test('auth gate opens a login modal for protected actions and resumes after login', async () => {
  const user = userEvent.setup();
  const onAuthenticated = vi.fn();
  const reload = vi.fn();
  setAuthGateReloadForTest(reload);

  authMocks.getAuthLoginOptions.mockResolvedValue({
    availableLoginMethods: ['phone'],
  });

  render(
    <AuthGate>
      <ProtectedActionButton onAuthenticated={onAuthenticated} />
    </AuthGate>,
  );

  await user.click(await screen.findByRole('button', { name: '进入作品' }));

  const dialog = screen.getByRole('dialog', { name: '账号入口' });
  expect(dialog).toBeTruthy();
  expect(screen.queryByText('先登录账号，再同步你的冒险进度。')).toBeNull();

  const phoneInput = within(dialog).getByLabelText(
    '手机号',
  ) as HTMLInputElement;
  const codeInput = within(dialog).getByLabelText(
    '验证码',
  ) as HTMLInputElement;
  expect(phoneInput.className).toContain('platform-text-field');
  expect(codeInput.className).toContain('platform-text-field');

  await user.type(phoneInput, '13800000000');
  await user.type(codeInput, '123456');
  await acceptLegalConsent(user, dialog);
  await user.click(within(dialog).getByRole('button', { name: '登录' }));

  await waitFor(() => {
    expect(authMocks.loginWithPhoneCode).toHaveBeenCalledWith(
      '13800000000',
      '123456',
    );
    expect(authMocks.getCurrentAuthUser).toHaveBeenCalledTimes(1);
    expect(onAuthenticated).toHaveBeenCalledTimes(1);
    expect(reload).toHaveBeenCalledTimes(1);
  });

  expect(screen.queryByRole('dialog', { name: '账号入口' })).toBeNull();
});

test('auth gate uses mini program auth bridge instead of opening login modal in mini program runtime', async () => {
  const user = userEvent.setup();
  authMocks.isWechatMiniProgramWebViewRuntime.mockReturnValue(true);
  authMocks.getAuthLoginOptions.mockResolvedValue({
    availableLoginMethods: ['phone', 'wechat'],
  });

  render(
    <AuthGate>
      <ProtectedActionButton onAuthenticated={vi.fn()} />
    </AuthGate>,
  );

  await user.click(await screen.findByRole('button', { name: '进入作品' }));

  await waitFor(() => {
    expect(authMocks.requestWechatMiniProgramPhoneLogin).toHaveBeenCalledTimes(
      1,
    );
  });
  expect(authMocks.startWechatLogin).not.toHaveBeenCalled();
  expect(screen.queryByRole('dialog', { name: '账号入口' })).toBeNull();
  expect(authMocks.isWechatMiniProgramWebViewRuntime).toHaveBeenCalled();
});

test('login modal requires first-time legal consent before sms login', async () => {
  const user = userEvent.setup();

  authMocks.getAuthLoginOptions.mockResolvedValue({
    availableLoginMethods: ['phone'],
  });

  render(
    <AuthGate>
      <ProtectedActionButton onAuthenticated={vi.fn()} />
    </AuthGate>,
  );

  await user.click(await screen.findByRole('button', { name: '进入作品' }));

  const dialog = screen.getByRole('dialog', { name: '账号入口' });
  await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
  await user.type(within(dialog).getByLabelText('验证码'), '123456');

  const loginButton = within(dialog).getByRole('button', { name: '登录' });
  const legalSwitch = within(dialog).getByRole('switch', {
    name: '同意法律协议',
  });
  expect((loginButton as HTMLButtonElement).disabled).toBe(true);
  expect(legalSwitch.getAttribute('aria-checked')).toBe('false');

  await user.click(
    within(dialog).getByRole('button', { name: '《用户协议》' }),
  );
  expect(await screen.findByRole('dialog', { name: '用户协议' })).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '我知道了' }));
  expect(legalSwitch.getAttribute('aria-checked')).toBe('false');

  await user.click(legalSwitch);
  expect(legalSwitch.getAttribute('aria-checked')).toBe('true');
  expect(window.localStorage.getItem(LEGAL_CONSENT_STORAGE_KEY)).toBe('true');
  expect((loginButton as HTMLButtonElement).disabled).toBe(false);
});

test('login modal defaults legal consent to checked after stored confirmation', async () => {
  const user = userEvent.setup();
  window.localStorage.setItem(LEGAL_CONSENT_STORAGE_KEY, 'true');

  authMocks.getAuthLoginOptions.mockResolvedValue({
    availableLoginMethods: ['phone'],
  });

  render(
    <AuthGate>
      <ProtectedActionButton onAuthenticated={vi.fn()} />
    </AuthGate>,
  );

  await user.click(await screen.findByRole('button', { name: '进入作品' }));

  const dialog = screen.getByRole('dialog', { name: '账号入口' });
  const legalSwitch = within(dialog).getByRole('switch', {
    name: '同意法律协议',
  });
  expect(legalSwitch.getAttribute('aria-checked')).toBe('true');
});

test('phone login result is not overwritten by an older guest hydrate', async () => {
  const user = userEvent.setup();
  const onAuthenticated = vi.fn();
  authMocks.getAuthLoginOptions.mockResolvedValue({
    availableLoginMethods: ['phone'],
  });
  authMocks.getCurrentAuthUser
    .mockResolvedValueOnce({
      user: null,
      availableLoginMethods: ['phone'],
    })
    .mockResolvedValue({
      user: mockUser,
      availableLoginMethods: ['phone'],
    });

  render(
    <AuthGate>
      <ProtectedActionButton onAuthenticated={onAuthenticated} />
      <LogoutStateProbe />
    </AuthGate>,
  );

  await user.click(await screen.findByRole('button', { name: '进入作品' }));
  const dialog = screen.getByRole('dialog', { name: '账号入口' });
  await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
  await user.type(within(dialog).getByLabelText('验证码'), '123456');
  await acceptLegalConsent(user, dialog);
  await user.click(within(dialog).getByRole('button', { name: '登录' }));

  expect(await screen.findByText('当前用户：测试玩家')).toBeTruthy();
  expect(onAuthenticated).toHaveBeenCalledTimes(1);

  expect(screen.getByText('当前用户：测试玩家')).toBeTruthy();
  expect(screen.queryByRole('dialog', { name: '账号入口' })).toBeNull();
});

test('auth gate hides register entry and opens invite modal for new sms account', async () => {
  const user = userEvent.setup();
  window.history.replaceState(null, '', '/?inviteCode=spring-2026');
  authMocks.getAuthLoginOptions.mockResolvedValue({
    availableLoginMethods: ['phone'],
  });
  authMocks.loginWithPhoneCode.mockResolvedValueOnce({
    token: 'jwt-phone-new',
    user: mockUser,
    created: true,
    referral: null,
  });

  render(
    <AuthGate>
      <div>公开内容</div>
      <ProtectedActionButton onAuthenticated={vi.fn()} />
    </AuthGate>,
  );

  expect(await screen.findByText('公开内容')).toBeTruthy();
  expect(screen.queryByRole('dialog', { name: '账号入口' })).toBeNull();

  await user.click(screen.getByRole('button', { name: '进入作品' }));
  const dialog = await screen.findByRole('dialog', { name: '账号入口' });
  expect(dialog.className).toContain('platform-auth-card');
  expect(dialog.className).toContain('platform-modal-shell');
  expect(within(dialog).queryByRole('tab', { name: '注册' })).toBeNull();
  expect(within(dialog).queryByLabelText('邀请码')).toBeNull();
  await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
  await user.type(within(dialog).getByLabelText('验证码'), '123456');
  await acceptLegalConsent(user, dialog);
  await user.click(within(dialog).getByRole('button', { name: '登录' }));

  await waitFor(() => {
    expect(authMocks.loginWithPhoneCode).toHaveBeenCalledWith(
      '13800000000',
      '123456',
    );
  });

  const inviteDialog = await screen.findByRole('dialog', {
    name: '请填写邀请码',
  });
  expect(inviteDialog.className).toContain('platform-auth-card');
  expect(inviteDialog.className).toContain('platform-modal-shell');
  const inviteCodeInput = within(inviteDialog).getByLabelText(
    '邀请码',
  ) as HTMLInputElement;
  expect(inviteCodeInput.value).toBe('SPRING2026');
  expect(inviteCodeInput.className).toContain('platform-text-field');
  expect(
    within(inviteDialog).getByRole('button', { name: '提交' }),
  ).toBeTruthy();

  await user.click(within(inviteDialog).getByRole('button', { name: '提交' }));

  await waitFor(() => {
    expect(authMocks.redeemRegistrationInviteCode).toHaveBeenCalledWith(
      'SPRING2026',
    );
  });
});

test('registration invite modal can skip when invite code is empty', async () => {
  const user = userEvent.setup();
  authMocks.getAuthLoginOptions.mockResolvedValue({
    availableLoginMethods: ['phone'],
  });
  authMocks.loginWithPhoneCode.mockResolvedValueOnce({
    token: 'jwt-phone-new',
    user: mockUser,
    created: true,
    referral: null,
  });

  render(
    <AuthGate>
      <ProtectedActionButton onAuthenticated={vi.fn()} />
    </AuthGate>,
  );

  await user.click(await screen.findByRole('button', { name: '进入作品' }));
  const dialog = screen.getByRole('dialog', { name: '账号入口' });
  await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
  await user.type(within(dialog).getByLabelText('验证码'), '123456');
  await acceptLegalConsent(user, dialog);
  await user.click(within(dialog).getByRole('button', { name: '登录' }));

  const inviteDialog = await screen.findByRole('dialog', {
    name: '请填写邀请码',
  });
  await user.click(within(inviteDialog).getByRole('button', { name: '跳过' }));

  await waitFor(() => {
    expect(screen.queryByRole('dialog', { name: '请填写邀请码' })).toBeNull();
  });
  expect(authMocks.redeemRegistrationInviteCode).not.toHaveBeenCalled();
});

test('auth state refresh keeps mounted platform content and local tab state', async () => {
  const user = userEvent.setup();
  const reload = vi.fn();
  setAuthGateReloadForTest(reload);
  authMocks.getCurrentAuthUser.mockResolvedValue({
    user: mockUser,
    availableLoginMethods: ['phone'],
  });

  render(
    <AuthGate>
      <PlatformTabStateProbe />
    </AuthGate>,
  );

  expect(await screen.findByText('当前Tab：首页')).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '创作' }));
  expect(screen.getByText('当前Tab：创作')).toBeTruthy();

  let resolveToken!: (token: string) => void;
  const tokenPromise = new Promise<string>((resolve) => {
    resolveToken = resolve;
  });
  authMocks.refreshStoredAccessToken.mockReturnValueOnce(tokenPromise);

  act(() => {
    window.dispatchEvent(new Event('genarrative-auth-state-changed'));
  });

  expect(screen.queryByText('正在校验登录状态...')).toBeNull();
  expect(screen.getByText('当前Tab：创作')).toBeTruthy();

  await act(async () => {
    resolveToken('jwt-refreshed-token');
    await tokenPromise;
  });

  await waitFor(() => {
    expect(authMocks.getCurrentAuthUser).toHaveBeenCalledTimes(2);
  });
  expect(screen.getByText('当前Tab：创作')).toBeTruthy();
  expect(reload).not.toHaveBeenCalled();
});

test('logout withdraws user context before backend request finishes', async () => {
  const user = userEvent.setup();
  const reload = vi.fn();
  setAuthGateReloadForTest(reload);
  authMocks.getCurrentAuthUser.mockResolvedValue({
    user: mockUser,
    availableLoginMethods: ['phone'],
  });

  let resolveLogout!: () => void;
  const logoutPromise = new Promise<void>((resolve) => {
    resolveLogout = resolve;
  });
  authMocks.logoutAuthUser.mockReturnValueOnce(logoutPromise);

  render(
    <AuthGate>
      <LogoutStateProbe />
    </AuthGate>,
  );

  expect(await screen.findByText('当前用户：测试玩家')).toBeTruthy();
  expect(screen.getByText('私有数据：可读取')).toBeTruthy();

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

  expect(await screen.findByText('当前用户：未登录')).toBeTruthy();
  expect(screen.getByText('私有数据：不可读取')).toBeTruthy();
  expect(authMocks.logoutAuthUser).toHaveBeenCalledTimes(1);
  expect(reload).not.toHaveBeenCalled();

  await act(async () => {
    resolveLogout();
    await logoutPromise;
  });

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

test('auth gate shows sms send feedback in the login modal', async () => {
  const user = userEvent.setup();

  authMocks.getAuthLoginOptions.mockResolvedValue({
    availableLoginMethods: ['phone'],
  });

  render(
    <AuthGate>
      <ProtectedActionButton onAuthenticated={vi.fn()} />
    </AuthGate>,
  );

  await user.click(await screen.findByRole('button', { name: '进入作品' }));

  const dialog = screen.getByRole('dialog', { name: '账号入口' });
  await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
  await user.click(within(dialog).getByRole('button', { name: '获取验证码' }));

  await waitFor(() => {
    expect(authMocks.sendPhoneLoginCode).toHaveBeenCalledWith(
      '13800000000',
      'login',
      {
        challengeId: undefined,
        answer: '',
      },
    );
  });

  expect(
    within(dialog).getByText('短信请求已提交，验证码有效期约 5 分钟。'),
  ).toBeTruthy();
  expect(within(dialog).getByRole('button', { name: '60s' })).toBeTruthy();
});

test('login modal resets draft state every time it is reopened', async () => {
  const user = userEvent.setup();

  authMocks.getAuthLoginOptions.mockResolvedValue({
    availableLoginMethods: ['phone', 'password'],
  });

  render(
    <AuthGate>
      <ProtectedActionButton onAuthenticated={vi.fn()} />
    </AuthGate>,
  );

  await user.click(await screen.findByRole('button', { name: '进入作品' }));

  const firstDialog = screen.getByRole('dialog', { name: '账号入口' });
  expect(firstDialog.className).toContain('platform-auth-card');
  expect(firstDialog.className).toContain('platform-modal-shell');
  await user.type(within(firstDialog).getByLabelText('手机号'), '13800000000');
  await user.click(
    within(firstDialog).getByRole('button', { name: '获取验证码' }),
  );

  expect(
    await within(firstDialog).findByText(
      '短信请求已提交，验证码有效期约 5 分钟。',
    ),
  ).toBeTruthy();
  await user.type(within(firstDialog).getByLabelText('验证码'), '123456');
  await user.click(within(firstDialog).getByRole('tab', { name: '密码登录' }));
  const passwordInput = within(firstDialog).getByLabelText(
    '密码',
  ) as HTMLInputElement;
  expect(passwordInput.className).toContain('platform-text-field');
  await user.type(passwordInput, 'passw0rd');
  await user.click(
    within(firstDialog).getByRole('button', { name: '忘记密码' }),
  );

  expect(screen.getByRole('dialog', { name: '重置密码' })).toBeTruthy();

  await user.click(screen.getByRole('button', { name: '关闭登录弹窗' }));

  await waitFor(() => {
    expect(screen.queryByRole('dialog', { name: '账号入口' })).toBeNull();
  });

  await user.click(screen.getByRole('button', { name: '进入作品' }));

  const reopenedDialog = screen.getByRole('dialog', { name: '账号入口' });
  expect(
    within(reopenedDialog)
      .getByRole('tab', { name: '短信登录' })
      .getAttribute('aria-selected'),
  ).toBe('true');
  expect(
    (within(reopenedDialog).getByLabelText('手机号') as HTMLInputElement).value,
  ).toBe('');
  expect(
    (within(reopenedDialog).getByLabelText('验证码') as HTMLInputElement).value,
  ).toBe('');
  expect(within(reopenedDialog).queryByLabelText('密码')).toBeNull();
  expect(
    within(reopenedDialog).queryByText(
      '短信请求已提交，验证码有效期约 5 分钟。',
    ),
  ).toBeNull();
  expect(
    within(reopenedDialog).getByRole('button', { name: '获取验证码' }),
  ).toBeTruthy();
});

test('auth gate separates sms and password login by tabs', async () => {
  const user = userEvent.setup();

  authMocks.getAuthLoginOptions.mockResolvedValue({
    availableLoginMethods: ['phone', 'password'],
  });

  render(
    <AuthGate>
      <ProtectedActionButton onAuthenticated={vi.fn()} />
    </AuthGate>,
  );

  await user.click(await screen.findByRole('button', { name: '进入作品' }));

  const dialog = screen.getByRole('dialog', { name: '账号入口' });
  expect(
    within(dialog)
      .getByRole('tab', { name: '短信登录' })
      .getAttribute('aria-selected'),
  ).toBe('true');
  expect(
    within(dialog)
      .getByRole('tab', { name: '短信登录' })
      .className.includes('h-12'),
  ).toBe(true);
  expect(
    within(dialog).getByRole('tablist', { name: '登录方式' }).className,
  ).toContain('bg-transparent');
  expect(within(dialog).queryByLabelText('密码')).toBeNull();

  await user.click(within(dialog).getByRole('tab', { name: '密码登录' }));

  expect(
    within(dialog)
      .getByRole('tab', { name: '密码登录' })
      .getAttribute('aria-selected'),
  ).toBe('true');
  expect(within(dialog).queryByLabelText('验证码')).toBeNull();

  await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
  await user.type(within(dialog).getByLabelText('密码'), 'passw0rd');
  await acceptLegalConsent(user, dialog);
  await user.click(within(dialog).getByRole('button', { name: '登录' }));

  await waitFor(() => {
    expect(authMocks.authEntry).toHaveBeenCalledWith('13800000000', 'passw0rd');
  });
});

test('auth gate revokes merged session group and refreshes sessions', async () => {
  const user = userEvent.setup();
  const initialSessions: AuthSessionSummary[] = [
    {
      sessionId: 'usess_remote',
      sessionIds: ['usess_remote', 'usess_remote_rotated'],
      sessionCount: 2,
      clientLabel: 'Windows / Chrome',
      ipMasked: '203.0.*.*',
      isCurrent: false,
      createdAt: '2026-05-01T10:00:00.000Z',
      lastSeenAt: '2026-05-01T10:30:00.000Z',
      expiresAt: '2026-06-01T10:30:00.000Z',
    },
  ];
  authMocks.getCurrentAuthUser.mockResolvedValue({
    user: mockUser,
    availableLoginMethods: ['phone'],
  });
  authMocks.getAuthSessions
    .mockResolvedValueOnce(initialSessions)
    .mockResolvedValueOnce([]);

  render(
    <AuthGate>
      <AccountPanelProbe />
    </AuthGate>,
  );

  await user.click(await screen.findByRole('button', { name: '打开账号面板' }));
  const accountDialog = await screen.findByRole('dialog', {
    name: '账号信息',
  });
  await user.click(
    within(accountDialog).getByRole('button', { name: '踢下线' }),
  );

  await waitFor(() => {
    expect(authMocks.revokeAuthSessions).toHaveBeenCalledWith([
      'usess_remote',
      'usess_remote_rotated',
    ]);
    expect(authMocks.getAuthSessions).toHaveBeenCalledTimes(2);
  });
});

test('auth gate clears account state after password change', async () => {
  const user = userEvent.setup();
  authMocks.getCurrentAuthUser.mockResolvedValue({
    user: mockUser,
    availableLoginMethods: ['phone'],
  });
  authMocks.getAuthSessions.mockResolvedValue([]);
  authMocks.changePassword.mockResolvedValue(mockUser);

  render(
    <AuthGate>
      <div>
        <LogoutStateProbe />
        <AccountPanelProbe />
      </div>
    </AuthGate>,
  );

  expect(await screen.findByText('当前用户：测试玩家')).toBeTruthy();
  await user.click(screen.getByRole('button', { name: '打开账号面板' }));

  const accountDialog = await screen.findByRole('dialog', {
    name: '账号信息',
  });
  await user.click(
    within(accountDialog).getByRole('button', { name: '修改密码' }),
  );

  const passwordDialog = await screen.findByRole('dialog', {
    name: '修改登录密码',
  });
  await user.type(
    within(passwordDialog).getByLabelText('当前密码'),
    'oldpass1',
  );
  await user.type(within(passwordDialog).getByLabelText('新密码'), 'newpass1');
  await user.click(
    within(passwordDialog).getByRole('button', { name: '确认修改密码' }),
  );

  await waitFor(() => {
    expect(authMocks.changePassword).toHaveBeenCalledWith(
      'oldpass1',
      'newpass1',
    );
    expect(screen.getByText('当前用户：未登录')).toBeTruthy();
  });
  expect(screen.queryByRole('dialog', { name: '账号信息' })).toBeNull();
});
