import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { act, cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import { MemoryRouter } from 'react-router-dom';
import type { AlertEvent, AlertRule, Page } from '../../api/types';
import AlertsPage from './AlertsPage';
import { ROUTER_FUTURE } from '../routing/routerConfig';

const mocks = vi.hoisted(() => ({
  alertSummaryV2: vi.fn(), alertEventsV2: vi.fn(), alertEventV2: vi.fn(), actOnAlertV2: vi.fn(),
  alertRulesV2: vi.fn(), metricCatalog: vi.fn(), alertNotificationsV2: vi.fn(), readAlertNotificationsV2: vi.fn(),
  saveAlertRuleV2: vi.fn(), setAlertRuleEnabledV2: vi.fn()
}));
const layout = vi.hoisted(() => ({ mobile: false }));
vi.mock('../../api/client', () => ({ api: mocks }));
vi.mock('../auth/AuthGate', () => ({ usePlatformSession: () => ({ session: { role: 'admin' } }) }));
vi.mock('../hooks/useMobileLayout', () => ({ useMobileLayout: () => layout.mobile }));

afterEach(() => {
  cleanup();
  layout.mobile = false;
  Object.values(mocks).forEach((mock) => mock.mockReset());
});

function alertEvent(id: string, vin: string, plate: string): AlertEvent {
  return {
    id, ruleId: 'speed-rule', ruleName: `${plate}速度告警`, ruleVersion: 1, severity: 'major', status: 'unprocessed',
    vin, plate, protocol: 'JT808', metric: 'speed_kmh', operator: 'gt', triggerValue: 90, threshold: 80, thresholdHigh: 0,
    unit: 'km/h', durationSec: 60, location: '测试位置', sourceEventId: `${id}-source`, eventAt: '2026-07-16T04:00:00Z', receivedAt: '2026-07-16T04:00:01Z', triggeredAt: '2026-07-16T04:00:00Z', recoveredAt: '', handler: '', version: 1, actions: []
  };
}

function alertRule(): AlertRule {
  return {
    id: 'speed-rule', name: '测试超速规则', description: '测试规则', severity: 'major', valueType: 'numeric',
    metric: 'speed_kmh', operator: 'gt', threshold: 80, thresholdHigh: 0, durationSec: 60,
    recoveryOperator: 'lte', recoveryThreshold: 75, repeatIntervalSec: 600,
    scopeProtocols: ['JT808'], scopeVins: [], scopeOems: [], scopeModels: [], scopeCompanies: [],
    notificationChannels: ['in_app'], enabled: true, version: 3,
    createdBy: 'admin', updatedBy: 'admin', createdAt: '', updatedAt: ''
  };
}

test('preserves an unsubmitted event filter draft across alert tab URL changes', async () => {
  mocks.alertSummaryV2.mockResolvedValue({ active: 0, unprocessed: 0, processing: 0, recovered: 0, closed: 0, ignored: 0, unreadNotifications: 0, asOf: '' });
  mocks.alertEventsV2.mockResolvedValue({ items: [], total: 0, limit: 20, offset: 0 });
  mocks.alertRulesV2.mockResolvedValue([]);
  mocks.metricCatalog.mockResolvedValue({ metrics: [], asOf: '' });
  mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts']}><AlertsPage /></MemoryRouter></QueryClientProvider>);

  const keyword = await screen.findByPlaceholderText('车牌 / VIN / 规则名称');
  fireEvent.change(keyword, { target: { value: '保留筛选草稿' } });
  fireEvent.click(screen.getByRole('tab', { name: /站内通知/ }));
  expect(await screen.findByText('仅站内通道具备真实送达与已读状态')).toBeInTheDocument();
  fireEvent.click(screen.getByRole('tab', { name: /告警事件/ }));

  expect(await screen.findByPlaceholderText('车牌 / VIN / 规则名称')).toHaveValue('保留筛选草稿');
});

test('loads only event dependencies on the default alert tab', async () => {
  mocks.alertSummaryV2.mockResolvedValue({ active: 0, unprocessed: 0, processing: 0, recovered: 0, closed: 0, ignored: 0, unreadNotifications: 0, asOf: '' });
  mocks.alertEventsV2.mockResolvedValue({ items: [], total: 0, limit: 20, offset: 0 });
  mocks.alertRulesV2.mockResolvedValue([]);
  mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts']}><AlertsPage /></MemoryRouter></QueryClientProvider>);

  await screen.findByText('当前筛选条件没有告警事件');
  expect(mocks.alertRulesV2).toHaveBeenCalledTimes(1);
  expect(mocks.metricCatalog).not.toHaveBeenCalled();
  expect(mocks.alertNotificationsV2).toHaveBeenCalledTimes(1);
  expect(mocks.alertNotificationsV2.mock.calls[0][0].toString()).toBe('unreadOnly=true&limit=1');
});

test('loads a paginated notification page and an independent unread total on direct entry', async () => {
  mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts?tab=notifications']}><AlertsPage /></MemoryRouter></QueryClientProvider>);

  await screen.findByText('仅站内通道具备真实送达与已读状态');
  await screen.findByText('暂无站内通知');
  expect(view.container.querySelector('.v2-alert-notifications')).toHaveClass('semi-card');
  expect(screen.getByRole('heading', { level: 5, name: '站内通知' })).toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-notification-empty.semi-empty')).toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-notification-pagination')).toBeInTheDocument();
  expect(screen.getByRole('combobox', { name: '每页通知数' })).toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-channel-collapse.semi-collapse')).toBeInTheDocument();
  expect(screen.getByText('3 个 · 未启用')).toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-channel-descriptions.semi-descriptions')).toBeInTheDocument();
  expect(mocks.alertRulesV2).not.toHaveBeenCalled();
  expect(mocks.metricCatalog).not.toHaveBeenCalled();
  expect(mocks.alertNotificationsV2).toHaveBeenCalledTimes(2);
  expect(mocks.alertNotificationsV2.mock.calls.map(([params]) => params.toString())).toEqual(expect.arrayContaining([
    'unreadOnly=true&limit=1',
    'limit=20&offset=0'
  ]));
});

test('ends notification mutation feedback with a visible retryable error instead of a silent pending state', async () => {
  mocks.alertNotificationsV2.mockResolvedValue({ items: [{ id: 7, eventId: 'event-7', title: '车辆告警', content: '测试告警', severity: 'major', read: false, createdAt: '2026-07-16T04:00:00Z' }], total: 1, limit: 100, offset: 0 });
  mocks.readAlertNotificationsV2.mockRejectedValue(new Error('通知状态更新超时'));
  const client = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } });
  render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts?tab=notifications']}><AlertsPage /></MemoryRouter></QueryClientProvider>);

  const markRead = await screen.findByRole('button', { name: '标为已读' });
  expect(markRead.closest('.v2-alert-notification-card.semi-card')).toBeInTheDocument();
  expect(markRead.closest('.v2-alert-notification-list')?.querySelector('article')).not.toBeInTheDocument();
  fireEvent.click(markRead);

  expect(await screen.findByText('通知状态更新超时')).toBeInTheDocument();
  expect(screen.getByRole('button', { name: '标为已读' })).toBeEnabled();
});

test('keeps the mobile notification title, count, and compact bulk action visible together', async () => {
  layout.mobile = true;
  mocks.alertNotificationsV2.mockResolvedValue({ items: [{ id: 8, eventId: 'event-8', title: '车辆告警', content: '移动端告警', severity: 'major', read: false, createdAt: '2026-07-16T04:00:00Z' }], total: 1, limit: 100, offset: 0 });
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts?tab=notifications']}><AlertsPage /></MemoryRouter></QueryClientProvider>);

  const heading = await screen.findByRole('heading', { level: 5, name: /通知列表/ });
  await waitFor(() => expect(heading).toHaveTextContent('1 条'));
  expect(heading.querySelector('.v2-alert-notification-total.semi-tag')).toBeInTheDocument();
  expect(screen.getByRole('button', { name: '本页已读' })).toBeInTheDocument();
});

test('clears old alert rows and inspector evidence when the event scope changes', async () => {
  const oldEvent = alertEvent('old-event', 'OLDVIN', '旧告警车牌');
  oldEvent.actions = [{ id: 1, action: 'detect', fromStatus: '', toStatus: 'unprocessed', actor: 'alert-evaluator', note: '规则首次命中', createdAt: '2026-07-16T04:00:00Z' }];
  const newEvent = alertEvent('new-event', 'NEWVIN', '新告警车牌');
  let resolveNew!: (value: Page<AlertEvent>) => void;
  mocks.alertSummaryV2.mockResolvedValue({ active: 1, unprocessed: 1, processing: 0, recovered: 0, closed: 0, ignored: 0, unreadNotifications: 0, asOf: '' });
  mocks.alertEventsV2.mockImplementation((query: { keyword?: string }) => query.keyword === 'OLDVIN'
    ? Promise.resolve({ items: [oldEvent], total: 1, limit: 20, offset: 0 })
    : new Promise<Page<AlertEvent>>((resolve) => { resolveNew = resolve; }));
  mocks.alertEventV2.mockImplementation((id: string) => Promise.resolve(id === oldEvent.id ? oldEvent : newEvent));
  mocks.alertRulesV2.mockResolvedValue([]);
  mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts?keyword=OLDVIN']}><AlertsPage /></MemoryRouter></QueryClientProvider>);

  expect((await screen.findAllByText('旧告警车牌')).length).toBeGreaterThan(0);
  for (const className of ['v2-alert-filter-card', 'v2-alert-metric-rail', 'v2-alert-table-card']) {
    expect(view.container.querySelector(`.${className}.semi-card`)).toBeInTheDocument();
  }
  expect(view.container.querySelector('.v2-alert-navigation')).toBeInTheDocument();
  expect(screen.getByLabelText('告警中心工作区')).toHaveClass('v2-workspace-command-bar', 'v2-alert-command-bar');
  expect(screen.getByRole('heading', { level: 5, name: '告警处置工作台' })).toBeInTheDocument();
  expect(screen.getByText('核验事件证据、治理规则并追踪通知状态')).toBeInTheDocument();
  expect(screen.getByText('可处置事件')).toBeInTheDocument();
  expect(screen.queryByRole('heading', { level: 2, name: '告警中心' })).not.toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-inspector')).not.toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-workspace')).not.toHaveClass('is-inspector-open');
  expect(view.container.querySelectorAll('.v2-alert-metric-rail .v2-workspace-metric-action.semi-button')).toHaveLength(4);
  expect(view.container.querySelectorAll('.v2-alert-metric-context-actions .semi-button')).toHaveLength(3);
  expect(screen.getByRole('button', { name: '查看未读通知，共 0 条' }).closest('[role="listitem"]')).toHaveClass('is-warning', 'is-secondary');
  expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).not.toHaveClass('is-compact');
  expect(screen.getByRole('columnheader', { name: '触发 / 阈值' })).toBeInTheDocument();
  expect(screen.getByRole('columnheader', { name: '位置' })).toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-event-table .v2-alert-protocol-tag.semi-tag')).toHaveTextContent('JT/T 808');
  expect(view.container.querySelector('.v2-alert-table-scroll > table')).not.toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-mobile-list')).not.toBeInTheDocument();
  expect(view.container.querySelector('time[datetime="2026-07-16T04:00:00Z"]')).toHaveTextContent('07-16 12:00:00');
  const desktopAlertRow = screen.getByTestId('alert-row-old-event');
  expect(desktopAlertRow).toHaveAttribute('role', 'button');
  expect(desktopAlertRow).toHaveAttribute('aria-expanded', 'false');
  fireEvent.keyDown(desktopAlertRow, { key: 'Enter' });

  expect(await screen.findByText('old-event')).toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-inspector.semi-card')).toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-workspace')).toHaveClass('is-inspector-open');
  expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).toHaveClass('is-compact');
  expect(screen.queryByRole('columnheader', { name: '触发 / 阈值' })).not.toBeInTheDocument();
  expect(screen.queryByRole('columnheader', { name: '位置' })).not.toBeInTheDocument();
  expect(screen.getByText('已展开处置详情 · 点击其他事件快速切换')).toBeInTheDocument();
  expect(desktopAlertRow).toHaveAttribute('aria-expanded', 'true');
  expect(view.container.querySelector('.v2-alert-focus-card[aria-label="告警处置摘要"]')).toHaveClass('v2-alert-detail-card');
  expect(screen.getByLabelText('告警事件上下文')).toHaveTextContent('数据来源JT/T 808触发时间07-16 12:00:00处理记录1 条');
  expect(screen.getByLabelText('告警触发证据')).toHaveTextContent('触发值90 km/h阈值条件> 80 km/h，持续 60 秒超限幅度+10 km/h');
  expect(screen.getByText('1 条记录').closest('.semi-tag')).toBeInTheDocument();
  expect(screen.getByText('可操作').closest('.semi-tag')).toBeInTheDocument();
  const dispositionCard = view.container.querySelector('.v2-alert-disposition-card');
  const progressCard = view.container.querySelector('.v2-alert-progress-card');
  expect(dispositionCard).toHaveAttribute('aria-label', '告警处置操作');
  expect(dispositionCard?.compareDocumentPosition(progressCard!)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
  expect(view.container.querySelector('.v2-alert-technical-collapse.semi-collapse')).toBeInTheDocument();
  expect(screen.getByText('技术详情')).toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-inspector-heading')).toHaveClass('v2-workspace-panel-header');
  expect(view.container.querySelectorAll('.v2-alert-descriptions.semi-descriptions')).toHaveLength(1);
  expect(screen.getByLabelText('告警处理进度')).toHaveClass('semi-timeline');
  expect(screen.getByText('规则首次命中')).toBeInTheDocument();
  fireEvent.click(screen.getByRole('button', { name: '关闭告警详情' }));
  expect(view.container.querySelector('.v2-alert-inspector')).not.toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-event-table.semi-table-wrapper')).not.toHaveClass('is-compact');
  expect(screen.getByRole('columnheader', { name: '触发 / 阈值' })).toBeInTheDocument();
  expect(screen.getByRole('columnheader', { name: '位置' })).toBeInTheDocument();
  expect(desktopAlertRow).toHaveAttribute('aria-expanded', 'false');
  fireEvent.click(desktopAlertRow);
  expect(await screen.findByText('old-event')).toBeInTheDocument();
  fireEvent.change(screen.getByPlaceholderText('车牌 / VIN / 规则名称'), { target: { value: 'NEWVIN' } });
  fireEvent.click(screen.getByRole('button', { name: '查询' }));

  expect(await screen.findByText('正在更新事件…')).toBeInTheDocument();
  expect(screen.queryByText('旧告警车牌')).not.toBeInTheDocument();
  expect(screen.queryByText('old-event')).not.toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-inspector')).not.toBeInTheDocument();
  expect(view.container.querySelector('.v2-alert-workspace')).not.toHaveClass('is-inspector-open');

  await act(async () => resolveNew({ items: [newEvent], total: 1, limit: 20, offset: 0 }));
  expect((await screen.findAllByText('新告警车牌')).length).toBeGreaterThan(0);
  expect(screen.queryByText('new-event')).not.toBeInTheDocument();
  fireEvent.click(screen.getByTestId('alert-row-new-event'));
  expect(await screen.findByText('new-event')).toBeInTheDocument();
  await waitFor(() => expect(screen.queryByText('正在更新事件…')).not.toBeInTheDocument());
});

test('keeps status shortcuts comparable while the event list is status-filtered', async () => {
  mocks.alertSummaryV2.mockResolvedValue({ active: 3, unprocessed: 2, processing: 1, recovered: 4, closed: 1, ignored: 0, unreadNotifications: 0, asOf: '' });
  mocks.alertEventsV2.mockResolvedValue({ items: [], total: 2, limit: 20, offset: 0 });
  mocks.alertRulesV2.mockResolvedValue([]);
  mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts?keyword=TESTVIN&status=unprocessed']}><AlertsPage /></MemoryRouter></QueryClientProvider>);

  await screen.findByText('当前筛选条件没有告警事件');
  const summaryScope = mocks.alertSummaryV2.mock.calls[0][0];
  expect(summaryScope).toEqual(expect.objectContaining({ keyword: 'TESTVIN' }));
  expect(summaryScope).not.toHaveProperty('status');
  expect(screen.getByRole('button', { name: '筛选全部事件，共 8 条' })).toHaveAttribute('aria-pressed', 'false');
  expect(screen.getByRole('button', { name: '筛选未处理，共 2 条' })).toHaveAttribute('aria-pressed', 'true');
  expect(screen.getByRole('button', { name: '已恢复 4' })).toBeInTheDocument();

  fireEvent.click(screen.getByRole('button', { name: '筛选全部事件，共 8 条' }));
  await waitFor(() => expect(mocks.alertEventsV2).toHaveBeenLastCalledWith(expect.not.objectContaining({ status: expect.anything() }), expect.anything()));
  expect(screen.getByRole('button', { name: '筛选全部事件，共 8 条' })).toHaveAttribute('aria-pressed', 'true');
});

test('renders only selectable Semi alert cards on mobile', async () => {
  layout.mobile = true;
  const event = alertEvent('mobile-event', 'MOBILEVIN', '粤A移动01');
  mocks.alertSummaryV2.mockResolvedValue({ active: 1, unprocessed: 1, processing: 0, recovered: 0, closed: 0, ignored: 0, unreadNotifications: 0, asOf: '' });
  mocks.alertEventsV2.mockResolvedValue({ items: [event], total: 1, limit: 20, offset: 0 });
  mocks.alertEventV2.mockResolvedValue(event);
  mocks.alertRulesV2.mockResolvedValue([]);
  mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts']}><AlertsPage /></MemoryRouter></QueryClientProvider>);

  const action = await screen.findByRole('button', { name: '查看 粤A移动01 粤A移动01速度告警 告警详情' });
  expect(action).toHaveClass('semi-button', 'v2-alert-mobile-action');
  expect(action.closest('.semi-card')).toHaveClass('v2-alert-mobile-card');
  expect(view.container.querySelector('.v2-alert-table-scroll.is-mobile-scroll')).toHaveAttribute('tabindex', '0');
  expect(view.container.querySelector('.v2-alert-table-scroll.is-mobile-scroll')).toHaveAttribute('aria-label', '告警事件列表，可上下滚动');
  expect(view.container.querySelector('.v2-alert-event-table')).not.toBeInTheDocument();
  const mobileMetrics = Array.from(view.container.querySelectorAll('.v2-alert-metric-rail .v2-workspace-metric-action'));
  expect(mobileMetrics).toHaveLength(2);
  expect(mobileMetrics.map((metric) => metric.textContent)).toEqual(['待处置1需要核验', '全部事件1当前范围']);
  expect(screen.getByRole('button', { name: '筛选待处置，共 1 条' })).toHaveAttribute('aria-pressed', 'false');
  expect(view.container.querySelector('.v2-alert-metric-context-actions')).not.toBeInTheDocument();
  expect(screen.queryByRole('button', { name: /查看未读通知/ })).not.toBeInTheDocument();
  expect(screen.getByRole('tab', { name: /站内通知/ })).toBeInTheDocument();
  expect(action).toHaveAttribute('aria-expanded', 'false');
  expect(action).toHaveTextContent('超限幅度+10 km/h');
  expect(action.querySelector('.v2-alert-protocol-tag.semi-tag')).toHaveTextContent('JT/T 808');
  expect(action).toHaveTextContent('核验并处置');
  fireEvent.click(action);
  expect(action).toHaveAttribute('aria-pressed', 'true');
  expect(action).toHaveAttribute('aria-expanded', 'true');
  expect(await screen.findByRole('dialog', { name: '告警事件详情' })).toBeInTheDocument();
  expect(document.querySelector('.v2-alert-detail-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-detail-sidesheet');
  expect(document.querySelector('.v2-alert-detail-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(90dvh, 800px)' });
  expect(document.querySelector('.v2-alert-detail-sidesheet .v2-workspace-config-title > .semi-tag')).toHaveTextContent('未处理');
  expect(document.querySelector('.v2-alert-detail-sidesheet .v2-alert-inspector-heading')).not.toBeInTheDocument();
  const mobileDisposition = document.querySelector('.v2-alert-detail-sidesheet .v2-alert-disposition-card');
  const mobileProgress = document.querySelector('.v2-alert-detail-sidesheet .v2-alert-progress-card');
  expect(mobileDisposition).toHaveAttribute('aria-label', '告警处置操作');
  expect(mobileDisposition?.compareDocumentPosition(mobileProgress!)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
  expect(screen.getByLabelText('告警触发证据')).toHaveTextContent('超限幅度+10 km/h');
  expect(await screen.findByText('mobile-event')).toBeInTheDocument();
  fireEvent.click(screen.getByRole('button', { name: '关闭告警详情' }));
  expect(screen.queryByText('mobile-event')).not.toBeInTheDocument();
  expect(action).toHaveAttribute('aria-expanded', 'false');
});

test('keeps the primary alert query compact and applies advanced filters from a SideSheet', async () => {
  mocks.alertSummaryV2.mockResolvedValue({ active: 0, unprocessed: 0, processing: 0, recovered: 0, closed: 0, ignored: 0, unreadNotifications: 0, asOf: '' });
  mocks.alertEventsV2.mockResolvedValue({ items: [], total: 0, limit: 20, offset: 0 });
  mocks.alertRulesV2.mockResolvedValue([alertRule()]);
  mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts']}><AlertsPage /></MemoryRouter></QueryClientProvider>);

  await screen.findByText('当前筛选条件没有告警事件');
  expect(view.container.querySelector('.v2-alert-filter-primary')).toBeInTheDocument();
  expect(screen.queryByLabelText('协议')).not.toBeInTheDocument();
    fireEvent.click(screen.getByRole('button', { name: /更多筛选/ }));

  const dialog = await screen.findByRole('dialog', { name: '告警高级筛选' });
  fireEvent.click(within(dialog).getByRole('combobox', { name: '协议' }));
  fireEvent.click(await screen.findByText('JT808'));
  fireEvent.click(within(dialog).getByRole('button', { name: '应用筛选' }));

  await waitFor(() => expect(mocks.alertEventsV2).toHaveBeenLastCalledWith(expect.objectContaining({ protocol: 'JT808' }), expect.anything()));
  expect(screen.getByRole('button', { name: /更多筛选 · 1/ })).toHaveAttribute('aria-expanded', 'false');
});

test('uses one focused Semi bottom SideSheet for the complete mobile event filter', async () => {
  layout.mobile = true;
  mocks.alertSummaryV2.mockResolvedValue({ active: 0, unprocessed: 0, processing: 0, recovered: 0, closed: 0, ignored: 0, unreadNotifications: 0, asOf: '' });
  mocks.alertEventsV2.mockResolvedValue({ items: [], total: 0, limit: 20, offset: 0 });
  mocks.alertRulesV2.mockResolvedValue([alertRule()]);
  mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts']}><AlertsPage /></MemoryRouter></QueryClientProvider>);

  await screen.findByText('当前筛选条件没有告警事件');
  const trigger = screen.getByRole('button', { name: /修改事件筛选/ });
  expect(trigger).toHaveAttribute('aria-expanded', 'false');
  fireEvent.click(trigger);

  const dialog = await screen.findByRole('dialog', { name: '告警事件筛选' });
  expect(document.querySelector('.v2-alert-mobile-filter-sidesheet')).toHaveClass('semi-sidesheet-bottom');
  expect(document.querySelector('.v2-alert-mobile-filter-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(82dvh, 690px)' });
  expect(within(dialog).getByRole('textbox', { name: /关键词/ })).toBeInTheDocument();
  expect(within(dialog).getByRole('combobox', { name: '协议' })).toBeInTheDocument();
  expect(within(dialog).getByLabelText('告警起始时间')).toBeInTheDocument();
  expect(within(dialog).getByLabelText('告警结束时间')).toBeInTheDocument();
  fireEvent.click(within(dialog).getByRole('button', { name: '重置条件' }));
  expect(screen.getByRole('dialog', { name: '告警事件筛选' })).toBeInTheDocument();
  fireEvent.click(within(dialog).getByRole('combobox', { name: '协议' }));
  fireEvent.click(await screen.findByText('JT808'));
  fireEvent.click(within(dialog).getByRole('button', { name: '应用并查询' }));

  await waitFor(() => expect(mocks.alertEventsV2).toHaveBeenLastCalledWith(expect.objectContaining({ protocol: 'JT808' }), expect.anything()));
  expect(screen.getByRole('button', { name: /修改事件筛选：已启用 1 项/ })).toHaveAttribute('aria-expanded', 'false');
});

test('keeps mobile rule selection focused and opens editing in a Semi bottom SideSheet', async () => {
  layout.mobile = true;
  mocks.alertRulesV2.mockResolvedValue([alertRule()]);
  mocks.metricCatalog.mockResolvedValue({
    metrics: [{ key: 'speed_kmh', label: '速度', unit: 'km/h', category: 'driving', valueType: 'numeric', protocols: ['JT808'], sourceFields: {}, searchable: true, chartable: true, alertable: true }],
    asOf: ''
  });
  mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts?tab=rules']}><AlertsPage /></MemoryRouter></QueryClientProvider>);

  const ruleItem = await screen.findByRole('button', { name: /测试超速规则/ });
  await waitFor(() => expect(ruleItem).toHaveAttribute('aria-pressed', 'true'));
  expect(screen.getByRole('region', { name: '告警规则列表' })).toBeInTheDocument();
  expect(ruleItem).toHaveAttribute('aria-haspopup', 'dialog');
  expect(ruleItem).toHaveAttribute('aria-expanded', 'false');
  expect(document.querySelector('.v2-alert-rule-editor')).not.toBeInTheDocument();

  fireEvent.click(ruleItem);
  const editor = await screen.findByRole('dialog', { name: '告警规则编辑' });
  expect(document.querySelector('.v2-alert-rule-editor-sidesheet')).toHaveClass('semi-sidesheet-bottom', 'v2-workspace-editor-sidesheet');
  expect(document.querySelector('.v2-alert-rule-editor-sidesheet .semi-sidesheet-inner')).toHaveStyle({ height: 'min(92dvh, 820px)' });
  expect(within(editor).getByDisplayValue('测试超速规则')).toBeInTheDocument();
  expect(within(editor).getByRole('button', { name: '保存规则' })).toBeInTheDocument();
  expect(within(editor).getByRole('heading', { name: '触发条件' })).toBeInTheDocument();
  expect(within(editor).getByRole('heading', { name: '恢复与提醒' })).toBeInTheDocument();
  expect(within(editor).getByRole('heading', { name: '适用范围与说明' })).toBeInTheDocument();
  expect(within(editor).queryByRole('region', { name: '告警规则模板' })).not.toBeInTheDocument();
  expect(ruleItem).toHaveAttribute('aria-expanded', 'true');

  fireEvent.click(within(editor).getByRole('button', { name: '关闭告警规则编辑' }));
  await waitFor(() => expect(document.querySelector('.v2-alert-rule-editor')).not.toBeInTheDocument());
  expect(document.querySelector('.v2-alert-rule-editor-sidesheet .semi-sidesheet-inner')).toHaveClass('semi-sidesheet-animation-content_hide_bottom');
  expect(ruleItem).toHaveAttribute('aria-expanded', 'false');

  const newRule = screen.getByRole('button', { name: /新建规则/ });
  expect(newRule).toHaveAttribute('aria-haspopup', 'dialog');
  fireEvent.click(newRule);
  await waitFor(() => expect(document.querySelector('.v2-alert-rule-editor')).toBeInTheDocument());
  const newEditor = screen.getByRole('dialog', { name: '告警规则编辑' });
  expect(within(newEditor).getByText('新建规则')).toBeInTheDocument();
  expect(within(newEditor).getByRole('region', { name: '告警规则模板' })).toBeInTheDocument();
  expect(newRule).toHaveAttribute('aria-expanded', 'true');
});

test('creates auditable drafts from simple offline and hydrogen rule templates', async () => {
  mocks.alertRulesV2.mockResolvedValue([alertRule()]);
  mocks.metricCatalog.mockResolvedValue({
    metrics: [
      { key: 'freshness_sec', label: '离线时长', unit: 's', category: 'quality', valueType: 'numeric', protocols: ['GB32960', 'JT808', 'YUTONG_MQTT'], sourceFields: {}, searchable: true, chartable: true, alertable: true },
      { key: 'hydrogen_concentration_percent', label: '最高氢浓度', unit: '%', category: 'fuel-cell', valueType: 'numeric', protocols: ['GB32960'], sourceFields: {}, searchable: true, chartable: true, alertable: true }
    ],
    asOf: ''
  });
  mocks.alertNotificationsV2.mockResolvedValue({ items: [], total: 0, limit: 100, offset: 0 });
  const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
  const view = render(<QueryClientProvider client={client}><MemoryRouter future={ROUTER_FUTURE} initialEntries={['/alerts?tab=rules']}><AlertsPage /></MemoryRouter></QueryClientProvider>);

  const ruleItem = await screen.findByRole('button', { name: /测试超速规则/ });
  expect(view.container.querySelector('.v2-alert-rule-list')).toHaveClass('semi-card');
  expect(view.container.querySelector('.v2-alert-rule-editor')).toHaveClass('semi-card');
  expect(view.container.querySelector('.v2-alert-rule-list-heading')).toHaveClass('v2-workspace-panel-header');
  expect(view.container.querySelector('.v2-alert-rule-editor-heading')).toHaveClass('v2-workspace-panel-header');
  expect(screen.getByRole('heading', { name: '触发条件' }).closest('.v2-rule-form-section')).toBeInTheDocument();
  expect(screen.getByRole('heading', { name: '恢复与提醒' }).closest('.v2-rule-form-section')).toBeInTheDocument();
  expect(screen.getByRole('heading', { name: '适用范围与说明' }).closest('.v2-rule-form-section')).toBeInTheDocument();
  expect(screen.getByRole('region', { name: '告警规则列表，可左右滚动选择' })).toHaveAttribute('tabindex', '0');
  expect(ruleItem).toHaveClass('semi-button', 'v2-alert-rule-item');
  await waitFor(() => expect(ruleItem).toHaveAttribute('aria-pressed', 'true'));
  expect(within(ruleItem).getByText('重要').closest('.semi-tag')).toBeTruthy();
  expect(within(ruleItem).getByText('已启用').closest('.semi-tag')).toBeTruthy();
  expect(screen.queryByRole('region', { name: '告警规则模板' })).not.toBeInTheDocument();

  fireEvent.click(screen.getByRole('button', { name: /新建规则/ }));
  const offlineTemplate = await screen.findByRole('button', { name: /离线超过 10 小时/ });
  expect(offlineTemplate).toHaveClass('semi-button', 'v2-alert-template-card');
  expect(within(offlineTemplate).getByText('GB32960 / JT808 / YUTONG_MQTT').closest('.semi-tag')).toBeTruthy();
  await waitFor(() => expect(offlineTemplate).toBeEnabled());
  fireEvent.click(offlineTemplate);
  expect(screen.getByDisplayValue('车辆离线超过 10 小时')).toBeInTheDocument();
  expect(screen.getByDisplayValue('36000')).toBeInTheDocument();
  expect(screen.getByDisplayValue('GB32960,JT808,YUTONG_MQTT')).toBeInTheDocument();

  fireEvent.click(screen.getByRole('button', { name: /最高氢浓度/ }));
  expect(screen.getByDisplayValue('最高氢浓度超限')).toBeInTheDocument();
  expect(screen.getByPlaceholderText('请按厂家标准填写百分比')).toBeRequired();
  expect(screen.getByPlaceholderText('请按厂家标准填写百分比')).toHaveValue(null);
  expect(screen.getByDisplayValue('GB32960')).toBeInTheDocument();
});
