import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
import { DevicePreview } from '@/pages/DevicePreview';
import { useOpencodeStore } from '@/stores/opencode';

const fetchProjectDevicePreviewMock = vi.hoisted(() => vi.fn());
const invokeIpcMock = vi.hoisted(() => vi.fn());

vi.mock('@/lib/device-preview', () => ({
  fetchProjectDevicePreview: (...args: unknown[]) => fetchProjectDevicePreviewMock(...args),
}));

vi.mock('@/lib/api-client', () => ({
  invokeIpc: (...args: unknown[]) => invokeIpcMock(...args),
}));

vi.mock('sonner', () => ({
  toast: { success: vi.fn(), error: vi.fn() },
}));

const project = {
  id: 'project-1',
  name: '星球小游戏',
  path: 'D:/projects/planet-game',
  createdAt: '2026-08-03T00:00:00.000Z',
  updatedAt: '2026-08-03T00:00:00.000Z',
  lastOpenedAt: '2026-08-03T00:00:00.000Z',
};

function NavigationProbe() {
  const location = useLocation();
  return <output data-testid="navigation-probe">{location.pathname}</output>;
}

function renderPage() {
  return render(
    <MemoryRouter initialEntries={['/device-preview']}>
      <Routes>
        <Route path="/device-preview" element={<DevicePreview />} />
        <Route path="*" element={<NavigationProbe />} />
      </Routes>
    </MemoryRouter>,
  );
}

describe('DevicePreview page', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    useOpencodeStore.setState({ activeProject: project });
    invokeIpcMock.mockResolvedValue(undefined);
    Object.defineProperty(navigator, 'clipboard', {
      configurable: true,
      value: { writeText: vi.fn().mockResolvedValue(undefined) },
    });
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  it('shows a scannable QR code and uses the same trusted URL for copy and external open', async () => {
    fetchProjectDevicePreviewMock.mockResolvedValue({
      state: 'ready',
      projectId: project.id,
      appId: 'planet-game',
      versionName: 'v0.7.0',
      reviewStatus: 'approved',
      launchUrl: 'https://square.nianxx.cn/apps/planet-game/',
      updatedAt: '2026-08-03T01:05:00.000Z',
      message: '真机预览已就绪。',
    });

    renderPage();

    expect(await screen.findByRole('img', { name: '星球小游戏 真机预览二维码' })).toBeInTheDocument();
    expect(screen.getByTestId('device-preview-status')).toHaveTextContent('可预览');
    expect(screen.getByText('https://square.nianxx.cn/apps/planet-game/')).toBeInTheDocument();

    fireEvent.click(screen.getByRole('button', { name: '复制预览链接' }));
    await waitFor(() => expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
      'https://square.nianxx.cn/apps/planet-game/',
    ));

    fireEvent.click(screen.getByRole('button', { name: '在系统浏览器打开' }));
    await waitFor(() => expect(invokeIpcMock).toHaveBeenCalledWith(
      'shell:openExternal',
      'https://square.nianxx.cn/apps/planet-game/',
    ));
  });

  it('keeps the page read-only when no deployed runtime exists', async () => {
    fetchProjectDevicePreviewMock.mockResolvedValue({
      state: 'not_deployed',
      projectId: project.id,
      message: '当前项目还没有与本机绑定的服务端预览版本。',
    });

    renderPage();

    expect(await screen.findByText('当前项目还没有与本机绑定的服务端预览版本。')).toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /生成|部署|上传/ })).not.toBeInTheDocument();
    fireEvent.click(screen.getAllByRole('button', { name: '返回 AI 对话' })[1]!);
    const probe = await screen.findByTestId('navigation-probe');
    expect(probe).toHaveTextContent('/opencode-chat');
  });

  it('keeps a building version unscannable and offers a return to the AI conversation', async () => {
    fetchProjectDevicePreviewMock.mockResolvedValue({
      state: 'building',
      projectId: project.id,
      appId: 'planet-game',
      versionName: 'v0.7.0',
      message: '服务端正在构建或审核预览版本。',
    });

    renderPage();

    expect(await screen.findByTestId('device-preview-status')).toHaveTextContent('生成中');
    expect(screen.queryByTestId('device-preview-qr')).not.toBeInTheDocument();
    expect(screen.getAllByRole('button', { name: '返回 AI 对话' })).toHaveLength(2);
    expect(screen.queryByRole('button', { name: '让 AI 生成预览' })).not.toBeInTheDocument();
  });

  it('explains that device preview needs an active project', () => {
    useOpencodeStore.setState({ activeProject: null });

    renderPage();

    expect(screen.getByRole('heading', { name: '先选择一个项目' })).toBeInTheDocument();
    expect(fetchProjectDevicePreviewMock).not.toHaveBeenCalled();
  });

  it('waits for a slow poll to finish before scheduling another request', async () => {
    let resolveSlowPoll: ((value: unknown) => void) | undefined;
    fetchProjectDevicePreviewMock
      .mockResolvedValueOnce({
        state: 'building',
        projectId: project.id,
        message: '服务端正在构建本次预览版本。',
      })
      .mockImplementationOnce(() => new Promise((resolve) => {
        resolveSlowPoll = resolve;
      }))
      .mockResolvedValue({
        state: 'building',
        projectId: project.id,
        message: '服务端正在构建本次预览版本。',
      });
    vi.useFakeTimers();

    renderPage();
    await act(async () => undefined);
    expect(screen.getByTestId('device-preview-status')).toHaveTextContent('生成中');

    await act(async () => {
      await vi.advanceTimersByTimeAsync(4_000);
    });
    expect(fetchProjectDevicePreviewMock).toHaveBeenCalledTimes(2);

    await act(async () => {
      await vi.advanceTimersByTimeAsync(12_000);
    });
    expect(fetchProjectDevicePreviewMock).toHaveBeenCalledTimes(2);

    await act(async () => {
      resolveSlowPoll?.({
        state: 'building',
        projectId: project.id,
        message: '服务端正在构建本次预览版本。',
      });
      await Promise.resolve();
    });
    await act(async () => {
      await vi.advanceTimersByTimeAsync(4_000);
    });
    expect(fetchProjectDevicePreviewMock).toHaveBeenCalledTimes(3);
  });

  it('removes a previously verified QR code when revalidation fails', async () => {
    fetchProjectDevicePreviewMock
      .mockResolvedValueOnce({
        state: 'ready',
        projectId: project.id,
        launchUrl: 'https://square.nianxx.cn/apps/planet-game/',
        message: '真机预览已就绪。',
      })
      .mockRejectedValueOnce(new Error('登录状态已失效'));

    renderPage();
    expect(await screen.findByTestId('device-preview-qr')).toBeInTheDocument();

    fireEvent.click(screen.getByRole('button', { name: '刷新状态' }));

    expect(await screen.findByRole('heading', { name: '无法核验预览状态' })).toBeInTheDocument();
    expect(screen.queryByTestId('device-preview-qr')).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: '复制预览链接' })).not.toBeInTheDocument();
  });

  it('ignores a stale response after the active project changes', async () => {
    let resolveFirst: ((value: unknown) => void) | undefined;
    fetchProjectDevicePreviewMock
      .mockImplementationOnce(() => new Promise((resolve) => {
        resolveFirst = resolve;
      }))
      .mockResolvedValueOnce({
        state: 'not_deployed',
        projectId: 'project-2',
        message: '新项目没有预览版本。',
      });
    renderPage();

    act(() => {
      useOpencodeStore.setState({
        activeProject: { ...project, id: 'project-2', name: '新项目', path: 'D:/projects/new' },
      });
    });
    expect(await screen.findByText('新项目没有预览版本。')).toBeInTheDocument();

    await act(async () => {
      resolveFirst?.({
        state: 'ready',
        projectId: project.id,
        launchUrl: 'https://square.nianxx.cn/apps/old-project/',
        message: '旧项目预览。',
      });
      await Promise.resolve();
    });

    expect(screen.queryByTestId('device-preview-qr')).not.toBeInTheDocument();
    expect(screen.getByText('新项目没有预览版本。')).toBeInTheDocument();
  });
});
