/* @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 { beforeEach, expect, test, vi } from 'vitest';

import type { AuthUser } from '../../services/authService';
import { AuthGate } from './AuthGate';
import { useAuthUi } from './AuthUiContext';

const authMocks = vi.hoisted(() => ({
  authEntry: vi.fn(),
  changePassword: vi.fn(),
  ensureStoredAccessToken: vi.fn(),
  getAuthLoginOptions: vi.fn(),
  getCurrentAuthUser: vi.fn(),
  loginWithPhoneCode: vi.fn(),
  logoutAllAuthSessions: vi.fn(),
  logoutAuthUser: vi.fn(),
  resetPassword: vi.fn(),
  sendPhoneLoginCode: vi.fn(),
  startWechatLogin: vi.fn(),
  consumeAuthCallbackResult: vi.fn(),
}));

vi.mock('../../services/apiClient', () => ({
  AUTH_STATE_EVENT: 'genarrative-auth-state-changed',
  ensureStoredAccessToken: authMocks.ensureStoredAccessToken,
}));

vi.mock('../../services/authService', () => ({
  authEntry: authMocks.authEntry,
  bindWechatPhone: vi.fn(),
  changePassword: authMocks.changePassword,
  changePhoneNumber: vi.fn(),
  consumeAuthCallbackResult: authMocks.consumeAuthCallbackResult,
  getStoredLastLoginPhone: vi.fn(() => ''),
  getAuthAuditLogs: vi.fn(),
  getAuthLoginOptions: authMocks.getAuthLoginOptions,
  getAuthRiskBlocks: vi.fn(),
  getCurrentAuthUser: authMocks.getCurrentAuthUser,
  getAuthSessions: vi.fn(),
  getCaptchaChallengeFromError: vi.fn(() => null),
  liftAuthRiskBlock: vi.fn(),
  loginWithPhoneCode: authMocks.loginWithPhoneCode,
  logoutAllAuthSessions: authMocks.logoutAllAuthSessions,
  logoutAuthUser: authMocks.logoutAuthUser,
  resetPassword: authMocks.resetPassword,
  revokeAuthSession: vi.fn(),
  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', () => ({
  AccountModal: () => null,
}));

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

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

beforeEach(() => {
  vi.clearAllMocks();
  authMocks.consumeAuthCallbackResult.mockReturnValue(null);
  authMocks.ensureStoredAccessToken.mockResolvedValue('jwt-existing-token');
  authMocks.getCurrentAuthUser.mockResolvedValue({
    user: null,
    availableLoginMethods: ['phone'],
  });
  authMocks.loginWithPhoneCode.mockResolvedValue(mockUser);
  authMocks.authEntry.mockResolvedValue(mockUser);
  authMocks.changePassword.mockResolvedValue(mockUser);
  authMocks.logoutAllAuthSessions.mockResolvedValue(undefined);
  authMocks.logoutAuthUser.mockResolvedValue(undefined);
  authMocks.resetPassword.mockResolvedValue(mockUser);
  authMocks.sendPhoneLoginCode.mockResolvedValue({
    cooldownSeconds: 60,
    expiresInSeconds: 300,
  });
  authMocks.startWechatLogin.mockResolvedValue(undefined);
});

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>
  );
}

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 access token refresh before exposing restored user content', async () => {
  let resolveToken!: (token: string) => void;
  const tokenPromise = new Promise<string>((resolve) => {
    resolveToken = resolve;
  });
  authMocks.ensureStoredAccessToken.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-restored-token');

  expect(await screen.findByText('应用内容')).toBeTruthy();
  expect(authMocks.ensureStoredAccessToken).toHaveBeenCalledTimes(1);
  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 opens a login modal for protected actions and resumes after login', async () => {
  const user = userEvent.setup();
  const onAuthenticated = vi.fn();

  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();

  await user.type(within(dialog).getByLabelText('手机号'), '13800000000');
  await user.type(within(dialog).getByLabelText('验证码'), '123456');
  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(screen.queryByRole('dialog', { name: '账号入口' })).toBeNull();
});

test('auth state refresh keeps mounted platform content and local tab state', async () => {
  const user = userEvent.setup();
  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.ensureStoredAccessToken.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();
});

test('logout withdraws user context before backend request finishes', async () => {
  const user = userEvent.setup();
  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);

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

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: '账号入口' });
  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: '密码登录' }));
  await user.type(within(firstDialog).getByLabelText('密码'), '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).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 user.click(within(dialog).getByRole('button', { name: '登录' }));

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