// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { AppShell, type InstanceContext } from './app-shell.js';
import type { AuditDataSource } from './audit/audit-page.js';
import type { EventStreamClient } from './events/event-stream-client.js';
import { type FleetDataSource, type FleetSnapshot } from './fleet/fleet-page.js';
import type { FleetMessagesDataSource } from './fleet/fleet-messages-api-data-source.js';
import type { InstanceDataSource } from './instances/instance-api-data-source.js';
import type { JobsDataSource } from './jobs/jobs-page.js';

afterEach(() => {
  cleanup();
  vi.restoreAllMocks();
  vi.unstubAllGlobals();
});

const snapshot: FleetSnapshot = {
  instances: [
    {
      id: 'bravo',
      name: 'Bravo',
      url: 'http://user:***@bravo.example:8080/private',
      tags: ['west'],
    },
    { id: 'alpha', name: 'Alpha', url: 'https://alpha.example/admin' },
  ],
  statuses: new Map([
    [
      'bravo',
      {
        reachable: true,
        authenticated: false,
        latencyMs: 40,
        summary: {
          version: '2.0',
          capabilities: ['sms'],
          freshness: 'stale',
          anomalies: ['clock drift'],
          resources: {
            cpuPercent: 18.4,
            memoryPercent: 63.2,
            maxTemperatureCelsius: 46.7,
            phoneNumbers: ['13800138000'],
          },
        },
      },
    ],
    [
      'alpha',
      {
        reachable: true,
        authenticated: true,
        latencyMs: 10,
        summary: { version: '1.0', capabilities: ['ussd'], freshness: 'fresh' },
      },
    ],
  ]),
};

function source(load: FleetDataSource['load']): FleetDataSource {
  return { load };
}

const quietEventStreamClient: EventStreamClient = {
  subscribe: () => () => undefined,
};

const emptyPage = { items: [], page: { page: 1, pageSize: 25, total: 0 } };

describe('React AppShell and Fleet vertical slice', () => {
  it('loads real injected data and renders canonical origins and owner routes without React key warnings', async () => {
    const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
    let resolve!: (value: FleetSnapshot) => void;
    const dataSource = source(() => new Promise((done) => (resolve = done)));
    render(<AppShell pathname="/fleet" version="0.1.0" fleetDataSource={dataSource} />);

    expect(screen.getByRole('status', { name: '实例加载状态' }).textContent).toContain(
      '正在加载实例',
    );
    resolve(snapshot);

    const alpha = await screen.findByRole('row', { name: /Alpha/ });
    expect(within(alpha).getByRole('link', { name: 'Alpha' }).getAttribute('href')).toBe(
      '/instances/alpha/overview',
    );
    expect(
      within(screen.getByRole('row', { name: /Bravo/ }))
        .getByRole('link', { name: '打开 Bravo 的源站' })
        .getAttribute('href'),
    ).toBe('http://bravo.example:8080');
    expect(screen.getByText('版本 0.1.0')).toBeTruthy();
    expect(
      consoleError.mock.calls.some((call) =>
        call.some((argument) => String(argument).includes('unique "key" prop')),
      ),
    ).toBe(false);
  });

  it('makes cards, add, detail, edit, and safe deletion available from the overview', async () => {
    const user = userEvent.setup();
    const remove = vi.fn<InstanceDataSource['delete']>().mockResolvedValue(undefined);
    const instanceDataSource: InstanceDataSource = {
      get: vi.fn().mockResolvedValue({
        id: 'bravo',
        name: 'Bravo',
        origin: 'http://bravo.example:8080',
        tags: ['west'],
        revision: 7,
        credentialConfigured: true,
      }),
      create: vi.fn(),
      update: vi.fn(),
      testConnection: vi.fn(),
      delete: remove,
    };

    const fleetMessagesDataSource: FleetMessagesDataSource = {
      load: vi.fn(async (instanceId) =>
        instanceId === 'bravo'
          ? {
              latest: {
                id: 'sms-1',
                direction: 'incoming',
                phoneNumber: '13900139000',
                content: '这是一条用于聚合页展示的最新短信内容',
                timestamp: '2026-07-19T08:30:00.000Z',
              },
            }
          : {},
      ),
    };

    render(
      <AppShell
        pathname="/fleet"
        fleetDataSource={source(async () => snapshot)}
        fleetMessagesDataSource={fleetMessagesDataSource}
        instanceDataSource={instanceDataSource}
      />,
    );

    expect(await screen.findByRole('region', { name: '实例卡片' })).toBeTruthy();
    expect(screen.getByRole('link', { name: '添加实例' }).getAttribute('href')).toBe(
      '/instances/new',
    );
    const card = screen.getByRole('article', { name: 'Bravo 实例概览' });
    expect(within(card).queryByText('延迟')).toBeNull();
    expect(within(card).queryByText('版本')).toBeNull();
    expect(within(card).queryByText('新鲜度')).toBeNull();
    expect(within(card).queryByText('40 ms')).toBeNull();
    expect(within(card).queryByText('2.0')).toBeNull();
    expect(within(card).queryByText('可能过期')).toBeNull();
    expect(within(card).getByText('18.4%')).toBeTruthy();
    expect(within(card).getByText('63.2%')).toBeTruthy();
    expect(within(card).getByText('46.7 °C')).toBeTruthy();
    expect(within(card).getByText('13800138000')).toBeTruthy();
    expect(await within(card).findByText('收到')).toBeTruthy();
    expect(within(card).getByText('13900139000')).toBeTruthy();
    expect(within(card).getByText(/这是一条用于聚合页展示/)).toBeTruthy();
    expect(within(card).getByText(/2026/)).toBeTruthy();
    expect(within(card).queryByText('能力未知')).toBeNull();
    expect(
      within(card).getByRole('link', { name: '打开 Bravo 实例仪表盘' }).getAttribute('href'),
    ).toBe('/instances/bravo/overview');
    expect(within(card).queryByRole('link', { name: /查看.*短信/ })).toBeNull();
    expect(within(card).getByRole('link', { name: '编辑 Bravo' }).getAttribute('href')).toBe(
      '/settings/instances/bravo',
    );

    await user.click(within(card).getByRole('button', { name: '删除 Bravo' }));
    expect(within(card).getByText('删除后无法撤销。')).toBeTruthy();
    await user.type(within(card).getByRole('textbox', { name: '输入 bravo 以确认删除' }), 'bravo');
    await user.click(within(card).getByRole('button', { name: '确认删除 Bravo' }));
    expect(remove).toHaveBeenCalledWith('bravo', 7);
    expect(within(card).getByRole('status').textContent).toContain('删除请求已提交');
  });

  it('keeps message read failures separate from instance reachability and reports each once', async () => {
    const messages: FleetMessagesDataSource = { load: vi.fn().mockRejectedValue(new Error('no')) };
    render(
      <AppShell
        pathname="/fleet"
        fleetDataSource={source(async () => snapshot)}
        fleetMessagesDataSource={messages}
      />,
    );
    const bravo = await screen.findByRole('article', { name: 'Bravo 实例概览' });
    expect(await within(bravo).findAllByText('短信暂不可用')).toHaveLength(1);
    expect(within(bravo).getByText('需要认证')).toBeTruthy();
  });

  it('keeps Jobs and Audit routes compatible without advertising them in global navigation', async () => {
    const jobsDataSource: JobsDataSource = { load: vi.fn().mockResolvedValue(emptyPage) };
    const { rerender } = render(
      <AppShell
        pathname="/jobs"
        jobsDataSource={jobsDataSource}
        eventStreamClient={quietEventStreamClient}
      />,
    );

    expect(await screen.findByText(/没有任务符合当前查询/i)).toBeTruthy();
    const navigation = screen.getByRole('navigation', { name: '全局导航' });
    expect(within(navigation).queryByRole('link', { name: '任务' })).toBeNull();
    expect(within(navigation).queryByRole('link', { name: '审计' })).toBeNull();
    expect(within(navigation).getByRole('link', { name: '实例总览' })).toBeTruthy();
    expect(within(navigation).getByRole('link', { name: '设置' })).toBeTruthy();

    const auditDataSource: AuditDataSource = { load: vi.fn().mockResolvedValue(emptyPage) };
    rerender(
      <AppShell
        pathname="/audit"
        auditDataSource={auditDataSource}
        eventStreamClient={quietEventStreamClient}
      />,
    );
    expect(await screen.findByText(/没有审计事件符合当前查询/i)).toBeTruthy();
  });

  it('loads route-owned instance context so overview-card navigation opens detail management', async () => {
    const instanceDataSource: InstanceDataSource = {
      get: vi.fn().mockResolvedValue({
        id: 'bravo',
        name: 'Bravo',
        origin: 'http://bravo.example:8080',
        tags: ['west'],
        revision: 7,
        credentialConfigured: true,
      }),
      create: vi.fn(),
      update: vi.fn(),
      testConnection: vi.fn(),
      delete: vi.fn(),
    };

    render(
      <AppShell pathname="/instances/bravo/overview" instanceDataSource={instanceDataSource} />,
    );

    expect(screen.getByRole('status').textContent).toContain('正在加载实例');
    expect(await screen.findByText('Bravo')).toBeTruthy();
    expect(screen.getByRole('heading', { name: '实例仪表盘' })).toBeTruthy();
    expect(screen.getByRole('link', { name: '编辑实例' }).getAttribute('href')).toBe(
      '/settings/instances/bravo',
    );
  });

  it('supports accessible search, status filtering, sorting, and visible selection', async () => {
    const user = userEvent.setup();
    render(<AppShell pathname="/fleet" fleetDataSource={source(async () => snapshot)} />);
    await screen.findByRole('row', { name: /Alpha/ });

    await user.click(screen.getByRole('button', { name: /按延迟排序/i }));
    let rows = screen.getAllByRole('row').slice(1);
    expect(rows[0]?.textContent).toContain('Alpha');
    await user.click(screen.getByRole('button', { name: /按延迟排序/i }));
    rows = screen.getAllByRole('row').slice(1);
    expect(rows[0]?.textContent).toContain('Bravo');

    await user.selectOptions(screen.getByRole('combobox', { name: '状态' }), 'auth');
    expect(screen.queryByRole('row', { name: /Alpha/ })).toBeNull();
    await user.click(screen.getByRole('checkbox', { name: '选择当前页全部实例' }));
    expect((screen.getByRole('checkbox', { name: '选择 Bravo' }) as HTMLInputElement).checked).toBe(
      true,
    );
    expect(screen.getByText('已选择 1 项')).toBeTruthy();

    await user.type(screen.getByRole('searchbox', { name: '搜索实例' }), 'missing');
    expect(screen.getByText(/没有实例符合搜索与筛选条件/i)).toBeTruthy();
  });

  it('exposes error, retry, configured-empty, and unsafe-origin states without claiming a backend', async () => {
    const user = userEvent.setup();
    const load = vi
      .fn<FleetDataSource['load']>()
      .mockRejectedValueOnce(new Error('collector unavailable'))
      .mockResolvedValueOnce({
        instances: [{ id: 'unsafe', name: 'Unsafe', url: 'javascript:alert(1)' }],
        statuses: new Map(),
      });
    const first = render(<AppShell pathname="/fleet" fleetDataSource={source(load)} />);

    expect((await screen.findByRole('alert')).textContent).toContain('实例加载失败');
    await user.click(screen.getByRole('button', { name: '重试加载实例' }));
    expect(await screen.findByRole('row', { name: /Unsafe/ })).toBeTruthy();
    expect(screen.queryByRole('link', { name: '打开 Unsafe 的源站' })).toBeNull();
    first.unmount();

    const { unmount } = render(
      <AppShell
        pathname="/fleet"
        fleetDataSource={source(async () => ({ instances: [], statuses: new Map() }))}
      />,
    );
    expect(await screen.findByText(/尚未配置实例/i)).toBeTruthy();
    unmount();
  });

  it('supports metadata filters, column visibility, pagination, and a batch-action entry', async () => {
    const user = userEvent.setup();
    render(<AppShell pathname="/fleet" fleetDataSource={source(async () => snapshot)} />);
    await screen.findByRole('row', { name: /Alpha/ });

    await user.selectOptions(screen.getByRole('combobox', { name: '能力' }), 'sms');
    expect(screen.queryByRole('row', { name: /Alpha/ })).toBeNull();
    expect(screen.getByRole('row', { name: /Bravo/ }).textContent).toContain('clock drift');

    await user.click(screen.getByRole('button', { name: '选择列' }));
    await user.click(screen.getByRole('checkbox', { name: '显示异常列' }));
    expect(screen.queryByRole('columnheader', { name: '异常' })).toBeNull();

    await user.click(screen.getByRole('checkbox', { name: '选择 Bravo' }));
    expect((screen.getByRole('button', { name: '批量操作' }) as HTMLButtonElement).disabled).toBe(
      false,
    );
    await user.click(screen.getByRole('button', { name: '批量操作' }));
    expect(screen.getByText('请为已选择的 1 项选择操作。')).toBeTruthy();
  });

  it('paginates fleet rows and selects only the current page', async () => {
    const many: FleetSnapshot = {
      instances: Array.from({ length: 11 }, (_, index) => ({
        id: `instance-${String(index + 1).padStart(2, '0')}`,
        name: `Instance ${String(index + 1).padStart(2, '0')}`,
        url: `https://instance-${index + 1}.example`,
      })),
      statuses: new Map(),
    };
    render(<AppShell pathname="/fleet" fleetDataSource={source(async () => many)} />);
    await screen.findByRole('row', { name: /Instance 01/ });

    expect(screen.queryByRole('row', { name: /Instance 11/ })).toBeNull();
    expect(screen.getByText('第 1 页，共 2 页')).toBeTruthy();
    fireEvent.click(screen.getByRole('checkbox', { name: '选择当前页全部实例' }));
    expect(screen.getByText('已选择 10 项')).toBeTruthy();
    fireEvent.click(screen.getByRole('button', { name: '下一页' }));
    expect(await screen.findByRole('row', { name: /Instance 11/ })).toBeTruthy();
    expect(screen.getByText('第 2 页，共 2 页')).toBeTruthy();
  });

  it('binds instance context to the route owner and never leaks mismatched context', () => {
    const instance: InstanceContext = {
      id: 'owner',
      name: 'Owner modem',
      origin: 'https://owner.example/path',
      status: 'online',
      authentication: 'authenticated',
      freshness: 'fresh',
    };
    const { rerender } = render(
      <AppShell pathname="/instances/owner/messages" instance={instance} />,
    );
    expect(screen.getByRole('heading', { name: '短信管理' })).toBeTruthy();
    expect(screen.getByText('Owner modem')).toBeTruthy();
    expect(screen.getByRole('link', { name: '打开源站' }).getAttribute('href')).toBe(
      'https://owner.example',
    );

    rerender(<AppShell pathname="/instances/someone-else/messages" instance={instance} />);
    expect(screen.queryByText('Owner modem')).toBeNull();
    expect(screen.getByRole('status').textContent).toContain('正在加载实例');
  });

  it.each([
    {
      pathname: '/jobs',
      endpoint: '/api/v1/jobs?page=1&pageSize=25&sort=createdAt&direction=desc',
      emptyMessage: /没有任务符合当前查询/i,
    },
    {
      pathname: '/audit',
      endpoint: '/api/v1/audit?sort=occurredAt&direction=desc&page=1&pageSize=25',
      emptyMessage: /没有审计事件符合当前查询/i,
    },
  ])(
    'uses the default HTTP data source for $pathname',
    async ({ pathname, endpoint, emptyMessage }) => {
      const fetcher = vi.fn<typeof fetch>().mockResolvedValue(
        new Response(JSON.stringify(emptyPage), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        }),
      );
      vi.stubGlobal('fetch', fetcher);

      render(<AppShell pathname={pathname} eventStreamClient={quietEventStreamClient} />);

      expect(await screen.findByText(emptyMessage)).toBeTruthy();
      expect(fetcher).toHaveBeenCalledTimes(1);
      expect(fetcher).toHaveBeenCalledWith(endpoint, {
        method: 'GET',
        credentials: 'same-origin',
        headers: { accept: 'application/json' },
        signal: expect.any(AbortSignal),
      });
    },
  );

  it.each([
    {
      pathname: '/jobs',
      sourceProp: 'jobsDataSource' as const,
      emptyMessage: /没有任务符合当前查询/i,
    },
    {
      pathname: '/audit',
      sourceProp: 'auditDataSource' as const,
      emptyMessage: /没有审计事件符合当前查询/i,
    },
  ])(
    'uses an explicitly injected data source instead of global fetch for $pathname',
    async ({ pathname, sourceProp, emptyMessage }) => {
      const fetcher = vi.fn<typeof fetch>();
      vi.stubGlobal('fetch', fetcher);
      const load = vi
        .fn<JobsDataSource['load'] | AuditDataSource['load']>()
        .mockResolvedValue(emptyPage);
      const injectedSource = { load };

      render(
        <AppShell
          pathname={pathname}
          eventStreamClient={quietEventStreamClient}
          {...(sourceProp === 'jobsDataSource'
            ? { jobsDataSource: injectedSource as JobsDataSource }
            : { auditDataSource: injectedSource as AuditDataSource })}
        />,
      );

      expect(await screen.findByText(emptyMessage)).toBeTruthy();
      expect(load).toHaveBeenCalledTimes(1);
      expect(fetcher).not.toHaveBeenCalled();
    },
  );
});
