import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import {
  AutomationUnauthorizedError,
  SearchExecutorBusyError,
  fetchSearchExecutorStatus,
  fetchSearchManualInterventions,
  openSearchManualIntervention,
  resolveAutomationBaseUrl,
  triggerSearchBatch,
} from '@/services/automationService';
import { getCurrentAccessToken } from '@/services/dataAccessScope';

vi.mock('@/services/dataAccessScope', () => ({
  getCurrentAccessToken: vi.fn(),
}));

describe('automationService', () => {
  const fetchMock = vi.fn<typeof fetch>();
  const getCurrentAccessTokenMock = vi.mocked(getCurrentAccessToken);

  beforeEach(() => {
    vi.stubGlobal('fetch', fetchMock);
    fetchMock.mockReset();
    getCurrentAccessTokenMock.mockResolvedValue(null);
  });

  afterEach(() => {
    vi.unstubAllGlobals();
    vi.clearAllMocks();
  });

  describe('resolveAutomationBaseUrl', () => {
    it('prefers explicit base url and trims trailing slash', () => {
      expect(resolveAutomationBaseUrl({
        explicitBaseUrl: 'http://localhost:3030/',
        isDev: false,
      })).toBe('http://localhost:3030');
    });

    it('uses proxy-relative requests in dev when no explicit base url is provided', () => {
      expect(resolveAutomationBaseUrl({
        isDev: true,
        protocol: 'http:',
        hostname: 'localhost',
        port: '3030',
      })).toBe('');
    });

    it('reuses the current hostname outside dev', () => {
      expect(resolveAutomationBaseUrl({
        isDev: false,
        protocol: 'http:',
        hostname: '127.0.0.1',
        port: '3030',
      })).toBe('http://127.0.0.1:3030');
    });

    it('prefers the current origin when running behind a reverse proxy', () => {
      expect(resolveAutomationBaseUrl({
        isDev: false,
        origin: 'https://example.com',
        protocol: 'https:',
        hostname: 'example.com',
      })).toBe('https://example.com');
    });

    it('falls back to 127.0.0.1 when no browser location is available', () => {
      expect(resolveAutomationBaseUrl({ isDev: false })).toBe('http://127.0.0.1:3030');
    });
  });

  it('normalizes search executor status responses', async () => {
    fetchMock.mockResolvedValue({
      ok: true,
      status: 200,
      json: async () => ({
        state: 'busy',
        canStart: false,
        canManage: true,
        status: 'processing',
        message: '已有搜索任务正在占用执行器（processing），请继续或停止后再试',
        task: {
          taskId: 'CG-001-ss-001',
          status: 'processing',
          batchId: 'CG-001',
          accountId: 'account-001',
          totalRows: 10,
          processedRows: 4,
          completedRows: 3,
          failedRows: 1,
          stoppedRows: 0,
          currentItem: '正在处理第 4 行',
          startedAt: '2026-04-21T08:00:00.000Z',
          pausedAt: null,
          finishedAt: null,
        },
      }),
    } as Response);

    await expect(fetchSearchExecutorStatus()).resolves.toMatchObject({
      state: 'busy',
      canManage: true,
      status: 'processing',
      task: {
        taskId: 'CG-001-ss-001',
        processedRows: 4,
      },
    });
  });

  it('sends the current Supabase access token as a Bearer authorization header', async () => {
    getCurrentAccessTokenMock.mockResolvedValue('token-xyz');
    fetchMock.mockResolvedValue({
      ok: true,
      status: 200,
      json: async () => ({
        state: 'idle',
        canStart: true,
        canManage: false,
        status: 'idle',
        message: '搜索执行器空闲',
      }),
    } as Response);

    await fetchSearchExecutorStatus();

    expect(fetchMock).toHaveBeenCalledWith('/api/search-executor-status', expect.objectContaining({
      headers: expect.any(Headers),
    }));
    const headers = (fetchMock.mock.calls[0][1] as RequestInit).headers as Headers;
    expect(headers.get('Authorization')).toBe('Bearer token-xyz');
  });

  it('throws AutomationUnauthorizedError for manual intervention list 401 responses', async () => {
    fetchMock.mockResolvedValue({
      ok: false,
      status: 401,
      json: async () => ({ error: '未登录或登录态已失效' }),
    } as Response);

    await expect(fetchSearchManualInterventions()).rejects.toBeInstanceOf(AutomationUnauthorizedError);
    await expect(fetchSearchManualInterventions()).rejects.toMatchObject({
      message: '未登录或登录态已失效',
      status: 401,
    });
  });

  it('throws AutomationUnauthorizedError when opening a manual intervention session gets 401', async () => {
    fetchMock.mockResolvedValue({
      ok: false,
      status: 401,
      json: async () => ({ error: '未登录或登录态已失效' }),
    } as Response);

    await expect(openSearchManualIntervention('task-401')).rejects.toBeInstanceOf(AutomationUnauthorizedError);
  });

  it('throws SearchExecutorBusyError when search start is blocked by the executor', async () => {
    fetchMock.mockResolvedValue({
      ok: false,
      status: 409,
      json: async () => ({
        code: 'SEARCH_EXECUTOR_BUSY',
        error: '已有搜索任务正在占用执行器（processing），请继续或停止后再试',
        executor: {
          state: 'busy',
          canStart: false,
          canManage: true,
          status: 'processing',
          message: '已有搜索任务正在占用执行器（processing），请继续或停止后再试',
          task: {
            taskId: 'CG-002-ss-001',
            status: 'processing',
            batchId: 'CG-002',
            accountId: 'account-002',
            totalRows: 20,
            processedRows: 8,
            completedRows: 8,
            failedRows: 0,
            stoppedRows: 0,
            currentItem: '正在处理第 9 行',
            startedAt: '2026-04-21T09:00:00.000Z',
            pausedAt: null,
            finishedAt: null,
          },
        },
      }),
    } as Response);

    await expect(triggerSearchBatch({
      batch_id: 'CG-002',
      account_id: 'account-002',
    })).rejects.toBeInstanceOf(SearchExecutorBusyError);

    await triggerSearchBatch({
      batch_id: 'CG-002',
      account_id: 'account-002',
    }).catch((error) => {
      expect(error).toBeInstanceOf(SearchExecutorBusyError);
      expect((error as SearchExecutorBusyError).executor.task?.taskId).toBe('CG-002-ss-001');
      expect((error as SearchExecutorBusyError).executor.status).toBe('processing');
    });
  });
});
