import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { act, renderHook, waitFor } from '@testing-library/react';
import type { PropsWithChildren } from 'react';
import { toast } from 'sonner';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { parseMaterialsFromExcel } from '@/lib/materialImportParser';
import { createMaterials, deleteMaterialsByIds } from '@/services/materialService';
import { recognizeMaterialsFromDocument } from '@/services/documentExtractionService';
import { deleteSourceFileById, updateSourceFileExtractionState, uploadSourceFile } from '@/services/sourceFileService';
import type { ProcurementMaterial, SourceFileRecord } from '@/types';

import { useMaterialMutations } from './useMaterialMutations';

vi.mock('@/lib/materialImportParser', () => ({
  parseMaterialsFromExcel: vi.fn(),
}));

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

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

vi.mock('@/services/sourceFileService', async () => {
  const actual = await vi.importActual<typeof import('@/services/sourceFileService')>('@/services/sourceFileService');
  return {
    ...actual,
    uploadSourceFile: vi.fn(),
    updateSourceFileExtractionState: vi.fn(),
    deleteSourceFileById: vi.fn(),
  };
});

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

function makeRecord(fileName: string, id = `source-${fileName}`): SourceFileRecord {
  return {
    id,
    batch_id: 'CG-2026-001',
    file_name: fileName,
    storage_path: `source-files/CG-2026-001/${fileName}`,
    mime_type: null,
    size_bytes: 10,
    uploaded_by: '测试用户',
    uploaded_at: '2026-04-26T09:00:00.000Z',
    extraction_status: 'uploaded',
    extraction_model: null,
    extraction_summary: null,
    extraction_error: null,
    extracted_material_count: null,
    extracted_at: null,
  };
}

function makeMaterial(overrides: Partial<ProcurementMaterial>): ProcurementMaterial {
  return {
    id: overrides.id ?? 'mat-001',
    material_name: overrides.material_name ?? '测试物料',
    specification: overrides.specification ?? '',
    quantity: overrides.quantity ?? 1,
    unit: overrides.unit ?? '个',
    technical_standard: overrides.technical_standard ?? '',
    material_requirement: overrides.material_requirement ?? '',
    technical_requirement: overrides.technical_requirement ?? '',
    applicable_conditions: overrides.applicable_conditions ?? '',
    test_requirement: overrides.test_requirement ?? '',
    acceptance_note: overrides.acceptance_note ?? '',
    warranty_period: overrides.warranty_period ?? '',
    deadline: overrides.deadline ?? '',
    invoice_type: overrides.invoice_type ?? '',
    brand: overrides.brand ?? '',
    remarks: overrides.remarks ?? '',
    delivery_address: overrides.delivery_address ?? '',
    source_file: overrides.source_file ?? 'source.xlsx',
    source_file_id: overrides.source_file_id,
    batch_id: overrides.batch_id ?? 'CG-2026-001',
    created_at: overrides.created_at ?? '2026-04-26T09:00:00.000Z',
    created_by: overrides.created_by,
  };
}

describe('useMaterialMutations upload routing', () => {
  let queryClient: QueryClient;

  const parseMaterialsFromExcelMock = vi.mocked(parseMaterialsFromExcel);
  const createMaterialsMock = vi.mocked(createMaterials);
  const recognizeMaterialsFromDocumentMock = vi.mocked(recognizeMaterialsFromDocument);
  const uploadSourceFileMock = vi.mocked(uploadSourceFile);
  const updateSourceFileExtractionStateMock = vi.mocked(updateSourceFileExtractionState);
  const deleteSourceFileByIdMock = vi.mocked(deleteSourceFileById);
  const deleteMaterialsByIdsMock = vi.mocked(deleteMaterialsByIds);
  const toastMock = vi.mocked(toast);

  beforeEach(() => {
    queryClient = new QueryClient({
      defaultOptions: {
        queries: { retry: false, gcTime: Infinity },
        mutations: { retry: false },
      },
    });
    uploadSourceFileMock.mockImplementation(async ({ file }) => makeRecord(file.name));
    updateSourceFileExtractionStateMock.mockImplementation(async (id, patch) => ({
      ...makeRecord('updated-file', id),
      ...patch,
    }));
    deleteMaterialsByIdsMock.mockResolvedValue(undefined);
    deleteSourceFileByIdMock.mockResolvedValue(null);
  });

  afterEach(() => {
    queryClient.clear();
    vi.clearAllMocks();
  });

  function wrapper({ children }: PropsWithChildren) {
    return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
  }

  function renderUploadHook() {
    return renderHook(() => useMaterialMutations({
      getUploadBatchId: () => 'CG-2026-001',
      getUser: () => ({
        id: 'user-001',
        username: 'tester',
        displayName: '测试用户',
        email: 'tester@example.com',
      }),
      uploadedMaterials: [],
      setUploadedMaterials: vi.fn(),
      getMaterials: () => [],
      setExpandedIds: vi.fn(),
      setSelectedKeys: vi.fn(),
      setEditingMaterial: vi.fn(),
      onUploadSuccess: vi.fn(),
    }), { wrapper });
  }

  function makeRateLimitError(fileName: string) {
    return Object.assign(
      new Error('模型调用达到速率限制，文件已保留为附件，请稍后重试识别。'),
      {
        name: 'DocumentExtractionError',
        status: 429,
        code: 'LLM_RATE_LIMIT',
        retryable: true,
        batchId: 'CG-2026-001',
        sourceFileName: fileName,
      },
    );
  }

  it('keeps Excel uploads local and does not call the document extraction API', async () => {
    const inserted = makeMaterial({
      id: 'mat-excel-001',
      material_name: 'Excel 物料',
      source_file: '采购清单.xlsx',
      source_file_id: 'source-采购清单.xlsx',
      created_by: 'user-001',
    });
    parseMaterialsFromExcelMock.mockResolvedValue([{
      material_name: 'Excel 物料',
      specification: 'DN50',
      quantity: 3,
      unit: '个',
      technical_standard: '',
      material_requirement: '',
      technical_requirement: '',
      applicable_conditions: '',
      test_requirement: '',
      acceptance_note: '',
      warranty_period: '',
      deadline: '',
      invoice_type: '',
      brand: '',
      remarks: '',
      delivery_address: '',
    }]);
    createMaterialsMock.mockResolvedValue([inserted]);

    const { result } = renderUploadHook();
    const file = new File(['excel bytes'], '采购清单.xlsx', {
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    });

    await act(async () => {
      await result.current.uploadMutation.mutateAsync([file]);
    });

    expect(parseMaterialsFromExcelMock).toHaveBeenCalledWith(file);
    expect(createMaterialsMock).toHaveBeenCalledTimes(1);
    expect(recognizeMaterialsFromDocumentMock).not.toHaveBeenCalled();
    expect(updateSourceFileExtractionStateMock).toHaveBeenCalledWith('source-采购清单.xlsx', {
      extraction_status: 'local_parsed',
      extracted_material_count: 1,
      extraction_error: null,
      extraction_model: null,
      extraction_summary: 'Excel 本地解析成功：1 项物料',
    });
  });

  it('sends non-Excel files to the backend recognition API and does not create materials in the frontend', async () => {
    const inserted = makeMaterial({
      id: 'mat-pdf-001',
      material_name: 'PDF 物料',
      source_file: '采购清单.pdf',
      source_file_id: 'source-采购清单.pdf',
      created_by: 'user-001',
    });
    recognizeMaterialsFromDocumentMock.mockResolvedValue({
      materials: [inserted],
      summary: '提取 1 项',
      warnings: [],
      elapsed: '1.0s',
      extractionModel: 'extraction-stage-model',
    });

    const { result } = renderUploadHook();
    const file = new File(['pdf bytes'], '采购清单.pdf', { type: 'application/pdf' });

    const uploadResult = await act(async () => result.current.uploadMutation.mutateAsync([file]));

    expect(updateSourceFileExtractionStateMock).toHaveBeenNthCalledWith(1, 'source-采购清单.pdf', {
      extraction_status: 'llm_processing',
      extraction_model: null,
      extraction_error: null,
    });
    expect(recognizeMaterialsFromDocumentMock).toHaveBeenCalledWith(file, {
      batchId: 'CG-2026-001',
      sourceFileId: 'source-采购清单.pdf',
      sourceFileName: '采购清单.pdf',
      createdBy: 'user-001',
    });
    expect(createMaterialsMock).not.toHaveBeenCalled();
    expect(updateSourceFileExtractionStateMock).toHaveBeenNthCalledWith(2, 'source-采购清单.pdf', {
      extraction_status: 'llm_succeeded',
      extraction_model: 'extraction-stage-model',
      extraction_summary: '提取 1 项',
      extraction_error: null,
      extracted_material_count: 1,
    });
    expect(uploadResult.materials).toEqual([inserted]);
    await waitFor(() => {
      expect(queryClient.getQueryData<ProcurementMaterial[]>(['materials-supabase']))
        .toEqual([inserted]);
    });
  });

  it('keeps successful document materials when a later Excel file fails', async () => {
    const inserted = makeMaterial({
      id: 'mat-pdf-001',
      material_name: 'PDF 物料',
      source_file: '采购清单.pdf',
      source_file_id: 'source-采购清单.pdf',
      created_by: 'user-001',
    });
    recognizeMaterialsFromDocumentMock.mockResolvedValue({
      materials: [inserted],
      summary: '提取 1 项',
      warnings: [],
      elapsed: '1.0s',
      extractionModel: 'extraction-stage-model',
    });
    parseMaterialsFromExcelMock.mockRejectedValue(new Error('Excel 文件损坏'));

    const { result } = renderUploadHook();
    const pdfFile = new File(['pdf bytes'], '采购清单.pdf', { type: 'application/pdf' });
    const excelFile = new File(['excel bytes'], '损坏.xlsx', {
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    });

    const uploadResult = await act(async () => result.current.uploadMutation.mutateAsync([pdfFile, excelFile]));

    expect(uploadResult.materials).toEqual([inserted]);
    expect(deleteMaterialsByIdsMock).not.toHaveBeenCalledWith(['mat-pdf-001']);
    expect(deleteSourceFileByIdMock).not.toHaveBeenCalledWith('source-采购清单.pdf');
  });

  it('records failed LLM extraction status while preserving the uploaded source file', async () => {
    recognizeMaterialsFromDocumentMock.mockRejectedValue(new Error('GLM 返回非法 JSON'));

    const { result } = renderUploadHook();
    const file = new File(['pdf bytes'], '采购清单.pdf', { type: 'application/pdf' });

    const uploadResult = await act(async () => result.current.uploadMutation.mutateAsync([file]));

    expect(uploadResult.failedDocumentCount).toBe(1);
    expect(deleteSourceFileByIdMock).not.toHaveBeenCalledWith('source-采购清单.pdf');
    expect(updateSourceFileExtractionStateMock).toHaveBeenNthCalledWith(1, 'source-采购清单.pdf', {
      extraction_status: 'llm_processing',
      extraction_model: null,
      extraction_error: null,
    });
    expect(updateSourceFileExtractionStateMock).toHaveBeenNthCalledWith(2, 'source-采购清单.pdf', {
      extraction_status: 'llm_failed',
      extraction_model: null,
      extraction_error: '文档识别失败: GLM 返回非法 JSON',
      extracted_material_count: 0,
    });
  });

  it('keeps Excel materials in cache and preserves failed non-Excel attachments during mixed uploads', async () => {
    const excelMaterial = makeMaterial({
      id: 'mat-excel-001',
      material_name: 'Excel 物料',
      source_file: '采购清单.xlsx',
      source_file_id: 'source-采购清单.xlsx',
      created_by: 'user-001',
    });
    parseMaterialsFromExcelMock.mockResolvedValue([{
      material_name: 'Excel 物料',
      specification: 'DN50',
      quantity: 3,
      unit: '个',
      technical_standard: '',
      material_requirement: '',
      technical_requirement: '',
      applicable_conditions: '',
      test_requirement: '',
      acceptance_note: '',
      warranty_period: '',
      deadline: '',
      invoice_type: '',
      brand: '',
      remarks: '',
      delivery_address: '',
    }]);
    createMaterialsMock.mockResolvedValue([excelMaterial]);
    recognizeMaterialsFromDocumentMock.mockRejectedValue(new Error('GLM 超时'));

    const { result } = renderUploadHook();
    const excelFile = new File(['excel bytes'], '采购清单.xlsx', {
      type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    });
    const pdfFile = new File(['pdf bytes'], '采购清单.pdf', { type: 'application/pdf' });

    const uploadResult = await act(async () => result.current.uploadMutation.mutateAsync([excelFile, pdfFile]));

    expect(uploadResult.materials).toEqual([excelMaterial]);
    expect(uploadResult.spreadsheetFileCount).toBe(1);
    expect(uploadResult.failedDocumentCount).toBe(1);
    expect(deleteSourceFileByIdMock).not.toHaveBeenCalledWith('source-采购清单.pdf');
    expect(updateSourceFileExtractionStateMock).toHaveBeenCalledWith('source-采购清单.pdf', {
      extraction_status: 'llm_failed',
      extraction_model: null,
      extraction_error: '文档识别失败: GLM 超时',
      extracted_material_count: 0,
    });
    expect(toastMock.error).toHaveBeenCalledWith(expect.stringContaining('文件 采购清单.pdf 物料识别失败: GLM 超时'));
    expect(toastMock.error).toHaveBeenCalledWith(
      expect.stringContaining('1 个文件识别失败'),
      expect.any(Object),
    );
    await waitFor(() => {
      expect(queryClient.getQueryData<ProcurementMaterial[]>(['materials-supabase']))
        .toEqual([excelMaterial]);
    });
  });

  it('summarizes multiple retryable LLM rate-limit failures after a mixed upload', async () => {
    const excelMaterial = makeMaterial({
      id: 'mat-excel-001',
      material_name: 'Excel 物料',
      source_file: '采购清单.xlsx',
      source_file_id: 'source-采购清单.xlsx',
      created_by: 'user-001',
    });
    parseMaterialsFromExcelMock.mockResolvedValue([{
      material_name: 'Excel 物料',
      specification: 'DN50',
      quantity: 1,
      unit: '个',
      technical_standard: '',
      material_requirement: '',
      technical_requirement: '',
      applicable_conditions: '',
      test_requirement: '',
      acceptance_note: '',
      warranty_period: '',
      deadline: '',
      invoice_type: '',
      brand: '',
      remarks: '',
      delivery_address: '',
    }]);
    createMaterialsMock.mockResolvedValue([excelMaterial]);
    recognizeMaterialsFromDocumentMock
      .mockRejectedValueOnce(makeRateLimitError('低压热缩附件.docx'))
      .mockRejectedValueOnce(makeRateLimitError('绝缘层2.png'));

    const { result } = renderUploadHook();
    const uploadResult = await act(async () => result.current.uploadMutation.mutateAsync([
      new File(['excel bytes'], '采购清单.xlsx', {
        type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      }),
      new File(['docx bytes'], '低压热缩附件.docx', {
        type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      }),
      new File(['png bytes'], '绝缘层2.png', { type: 'image/png' }),
    ]));

    expect(uploadResult.failedDocumentCount).toBe(2);
    expect(uploadResult.failedDocuments).toEqual([
      expect.objectContaining({
        fileName: '低压热缩附件.docx',
        code: 'LLM_RATE_LIMIT',
        retryable: true,
      }),
      expect.objectContaining({
        fileName: '绝缘层2.png',
        code: 'LLM_RATE_LIMIT',
        retryable: true,
      }),
    ]);
    expect(toastMock.error).toHaveBeenCalledWith(
      expect.stringContaining('2 个文件识别失败：LLM 调用达到速率限制'),
      expect.objectContaining({ duration: expect.any(Number) }),
    );
    expect(toastMock.error).toHaveBeenCalledWith(
      expect.stringContaining('附件已保留，请稍后重试'),
      expect.any(Object),
    );
    expect(queryClient.getQueryData<ProcurementMaterial[]>(['materials-supabase'])).toEqual([excelMaterial]);
  });
});
