import { act, cleanup, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import {
  AutomationRequestError,
  AutomationUnauthorizedError,
  fetchSearchManualInterventions,
  openSearchManualIntervention,
  type SearchManualInterventionAlert,
} from '@/services/automationService';
import { SearchManualInterventionProvider, useSearchManualIntervention } from '@/contexts/SearchManualInterventionContext';

const authState = vi.hoisted(() => ({
  current: {
    isAuthenticated: false,
    isBootstrapping: true,
    session: null as { token: string } | null,
  },
}));

vi.mock('@/hooks/useAuth', () => ({
  useAuth: () => authState.current,
}));

vi.mock('@/services/automationService', async () => {
  const actual = await vi.importActual<typeof import('@/services/automationService')>('@/services/automationService');
  return {
    ...actual,
    fetchSearchManualInterventions: vi.fn(),
    openSearchManualIntervention: vi.fn(),
  };
});

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

function createAlert(overrides: Partial<SearchManualInterventionAlert> = {}): SearchManualInterventionAlert {
  return {
    alertId: overrides.alertId ?? 'alert-1',
    taskId: overrides.taskId ?? 'task-1',
    batchId: overrides.batchId ?? 'CG-TEST-001',
    accountId: overrides.accountId ?? 'account-1',
    accountName: overrides.accountName ?? '测试账号',
    status: overrides.status ?? 'waiting_for_manual',
    headless: overrides.headless ?? true,
    message: overrides.message ?? '检测到验证码，等待人工处理',
    kind: overrides.kind ?? 'captcha',
    materialId: overrides.materialId ?? 'material-1',
    materialName: overrides.materialName ?? '测试物料',
    pageUrl: overrides.pageUrl ?? '',
    detectedAt: overrides.detectedAt ?? '2026-04-24T10:00:00.000Z',
    resolvedAt: overrides.resolvedAt ?? null,
    updatedAt: overrides.updatedAt ?? '2026-04-24T10:00:00.000Z',
    active: overrides.active ?? true,
  };
}

function Probe() {
  const { activeCount } = useSearchManualIntervention();
  return <div data-testid="active-count">{activeCount}</div>;
}

function renderProvider() {
  return render(
    <SearchManualInterventionProvider>
      <Probe />
    </SearchManualInterventionProvider>,
  );
}

async function flushAsyncWork() {
  await act(async () => {
    await Promise.resolve();
    await Promise.resolve();
  });
}

describe('SearchManualInterventionProvider', () => {
  const fetchSearchManualInterventionsMock = vi.mocked(fetchSearchManualInterventions);
  const openSearchManualInterventionMock = vi.mocked(openSearchManualIntervention);

  beforeEach(() => {
    vi.useFakeTimers();
    Object.defineProperty(window.HTMLMediaElement.prototype, 'play', {
      configurable: true,
      value: vi.fn(() => Promise.resolve()),
    });
    fetchSearchManualInterventionsMock.mockReset();
    fetchSearchManualInterventionsMock.mockResolvedValue({ alerts: [] });
    openSearchManualInterventionMock.mockReset();
    authState.current = {
      isAuthenticated: false,
      isBootstrapping: true,
      session: null,
    };
  });

  afterEach(() => {
    cleanup();
    vi.clearAllTimers();
    vi.useRealTimers();
    vi.clearAllMocks();
  });

  it('does not poll while auth state is still bootstrapping', () => {
    renderProvider();

    expect(fetchSearchManualInterventionsMock).not.toHaveBeenCalled();

    act(() => {
      vi.advanceTimersByTime(6000);
    });

    expect(fetchSearchManualInterventionsMock).not.toHaveBeenCalled();
    expect(screen.getByTestId('active-count')).toHaveTextContent('0');
  });

  it('polls only after the user is authenticated with a session token', async () => {
    authState.current = {
      isAuthenticated: true,
      isBootstrapping: false,
      session: { token: 'token-xyz' },
    };

    renderProvider();

    await flushAsyncWork();
    expect(fetchSearchManualInterventionsMock).toHaveBeenCalledTimes(1);

    await act(async () => {
      vi.advanceTimersByTime(2000);
      await Promise.resolve();
    });

    expect(fetchSearchManualInterventionsMock).toHaveBeenCalledTimes(2);
  });

  it('clears alerts and pauses polling after a manual intervention 401', async () => {
    authState.current = {
      isAuthenticated: true,
      isBootstrapping: false,
      session: { token: 'expired-token' },
    };
    fetchSearchManualInterventionsMock
      .mockResolvedValueOnce({ alerts: [createAlert()] })
      .mockRejectedValueOnce(new AutomationUnauthorizedError('未登录或登录态已失效'));

    renderProvider();

    await flushAsyncWork();
    expect(screen.getByTestId('active-count')).toHaveTextContent('1');

    await act(async () => {
      vi.advanceTimersByTime(2000);
      await Promise.resolve();
    });

    expect(screen.getByTestId('active-count')).toHaveTextContent('0');

    await act(async () => {
      vi.advanceTimersByTime(6000);
      await Promise.resolve();
    });

    expect(fetchSearchManualInterventionsMock).toHaveBeenCalledTimes(2);
  });

  it('keeps the existing alert during a transient automation 503', async () => {
    authState.current = {
      isAuthenticated: true,
      isBootstrapping: false,
      session: { token: 'token-xyz' },
    };
    fetchSearchManualInterventionsMock
      .mockResolvedValueOnce({ alerts: [createAlert()] })
      .mockRejectedValueOnce(new AutomationRequestError('Service Unavailable', 503));

    renderProvider();

    await flushAsyncWork();
    expect(screen.getByTestId('active-count')).toHaveTextContent('1');

    await act(async () => {
      vi.advanceTimersByTime(2000);
      await Promise.resolve();
    });

    expect(screen.getByTestId('active-count')).toHaveTextContent('1');
    expect(fetchSearchManualInterventionsMock).toHaveBeenCalledTimes(2);
  });

  it('does not replay the sound when only the active alert message changes', async () => {
    authState.current = {
      isAuthenticated: true,
      isBootstrapping: false,
      session: { token: 'token-xyz' },
    };
    const playMock = vi.mocked(window.HTMLMediaElement.prototype.play);
    fetchSearchManualInterventionsMock
      .mockResolvedValueOnce({ alerts: [createAlert({ message: '检测到验证码，等待人工处理' })] })
      .mockResolvedValueOnce({ alerts: [createAlert({ message: '正在切换到可视化验证码窗口，请稍候...', updatedAt: '2026-04-24T10:00:05.000Z' })] });

    renderProvider();

    await flushAsyncWork();
    expect(playMock).toHaveBeenCalledTimes(1);

    await act(async () => {
      vi.advanceTimersByTime(2000);
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(screen.getByTestId('active-count')).toHaveTextContent('1');
    expect(playMock).toHaveBeenCalledTimes(1);
  });

  it('refreshes alerts after opening a manual intervention fails', async () => {
    authState.current = {
      isAuthenticated: true,
      isBootstrapping: false,
      session: { token: 'token-xyz' },
    };
    const staleAlert = createAlert();
    fetchSearchManualInterventionsMock
      .mockResolvedValueOnce({ alerts: [staleAlert] })
      .mockResolvedValueOnce({ alerts: [] });
    openSearchManualInterventionMock.mockRejectedValueOnce(new AutomationRequestError('当前搜索任务不在等待人工处理状态', 400));

    function OpenProbe() {
      const { activeCount, latestAlert, openAlert } = useSearchManualIntervention();
      return (
        <>
          <div data-testid="active-count">{activeCount}</div>
          <button
            type="button"
            onClick={() => { if (latestAlert) void openAlert(latestAlert).catch(() => undefined); }}
          >
            open
          </button>
        </>
      );
    }

    render(
      <SearchManualInterventionProvider>
        <OpenProbe />
      </SearchManualInterventionProvider>,
    );

    await flushAsyncWork();
    expect(screen.getByTestId('active-count')).toHaveTextContent('1');

    await act(async () => {
      screen.getByRole('button', { name: 'open' }).click();
      await Promise.resolve();
      await Promise.resolve();
    });

    expect(openSearchManualInterventionMock).toHaveBeenCalledWith(staleAlert.taskId);
    expect(fetchSearchManualInterventionsMock).toHaveBeenCalledTimes(2);
    expect(screen.getByTestId('active-count')).toHaveTextContent('0');
  });
});
