import * as fs from 'node:fs';
import * as path from 'node:path';
import type { Page } from '@playwright/test';
import type { FileUploadOptions } from './types';
import { throwError } from './errors';
import { DEFAULT_TIMEOUTS } from './constants';

/**
 * 文件名参数类型（单文件或多文件）
 *
 * @internal
 */
type FileNames = string | string[];

/**
 * 上传文件到指定输入框（单文件）
 *
 * @description
 * 从 fixtures 目录加载单个测试文件并上传到指定的文件输入框。
 *
 * @param page - Playwright Page 对象
 * @param selector - 文件输入框的选择器
 * @param fileName - 要上传的文件名（相对于 fixtures 目录）
 * @param options - 可选配置
 *
 * @example
 * ```ts
 * await uploadFileToField(page, 'photo-upload', 'sample-id-card.jpg');
 * ```
 */
export async function uploadFileToField(
  page: Page,
  selector: string,
  fileName: string,
  options?: FileUploadOptions
): Promise<void>;

/**
 * 上传多个文件到指定输入框（多文件）
 *
 * @description
 * 从 fixtures 目录加载多个测试文件并一次性上传到指定的文件输入框。
 * 适用于 `<input type="file" multiple>` 场景。
 *
 * @param page - Playwright Page 对象
 * @param selector - 文件输入框的选择器
 * @param fileNames - 要上传的文件名数组（相对于 fixtures 目录）
 * @param options - 可选配置
 *
 * @example
 * ```ts
 * await uploadFileToField(page, 'photo-upload', [
 *   'sample-id-card.jpg',
 *   'sample-disability-card.jpg'
 * ]);
 * ```
 */
export async function uploadFileToField(
  page: Page,
  selector: string,
  fileNames: string[],
  options?: FileUploadOptions
): Promise<void>;

/**
 * 上传文件到指定输入框（统一实现）
 *
 * @description
 * 从 fixtures 目录加载测试文件并上传到指定的文件输入框。
 * 支持单文件和多文件上传，自动处理文件路径解析、存在性检查和错误处理。
 *
 * 支持的选择器策略：
 * - `data-testid` - 推荐，最稳定
 * - `aria-label` - 无障碍属性
 * - CSS 选择器 - 直接使用
 *
 * @param page - Playwright Page 对象
 * @param selector - 文件输入框的选择器
 * @param fileNames - 要上传的文件名（或文件名数组，相对于 fixtures 目录）
 * @param options - 可选配置
 * @param options.fixturesDir - fixtures 目录路径，默认为 'web/tests/fixtures'
 * @param options.timeout - 超时时间（毫秒），默认 5000ms
 * @param options.waitForUpload - 是否等待上传完成，默认为 true
 * @throws {E2ETestError} 当文件不存在或选择器无效时
 *
 * @example
 * ```ts
 * // 单文件上传
 * await uploadFileToField(page, 'photo-upload', 'sample-id-card.jpg');
 *
 * // 多文件上传
 * await uploadFileToField(page, 'photo-upload', [
 *   'sample-id-card.jpg',
 *   'sample-disability-card.jpg'
 * ]);
 *
 * // 自定义 fixtures 目录
 * await uploadFileToField(page, 'photo-upload', 'sample-id-card.jpg', {
 *   fixturesDir: 'custom/fixtures/path'
 * });
 * ```
 */
export async function uploadFileToField(
  page: Page,
  selector: string,
  fileNames: FileNames,
  options?: FileUploadOptions
): Promise<void> {
  // 检查是否为多文件上传
  const isMultiple = Array.isArray(fileNames);
  const fileCount = isMultiple ? fileNames.length : 1;

  console.debug(`[uploadFileToField] 开始上传: selector="${selector}", ${isMultiple ? `fileNames=[${fileNames.join(', ')}]` : `fileName="${fileNames}"`}`);

  // 1. 合并默认配置
  const config = {
    timeout: options?.timeout ?? DEFAULT_TIMEOUTS.static,
    fixturesDir: options?.fixturesDir ?? 'web/tests/fixtures',
    waitForUpload: options?.waitForUpload ?? true
  };

  // 2. 验证文件列表不为空（多文件场景）
  if (isMultiple && fileNames.length === 0) {
    throwError({
      operation: 'uploadFileToField',
      target: `选择器 "${selector}"`,
      expected: '至少提供一个文件路径',
      suggestion: '文件列表不能为空，请至少提供一个文件名'
    });
  }

  // 3. 解析所有文件路径
  const filePaths: string[] = [];
  const missingFiles: string[] = [];

  if (isMultiple) {
    // 多文件：解析每个文件路径
    for (const fileName of fileNames) {
      const filePath = resolveFixturePath(fileName, config.fixturesDir);
      if (fs.existsSync(filePath)) {
        filePaths.push(filePath);
      } else {
        missingFiles.push(fileName);
      }
    }
  } else {
    // 单文件：解析文件路径（fileNames 此时是 string）
    const filePath = resolveFixturePath(fileNames as string, config.fixturesDir);
    if (fs.existsSync(filePath)) {
      filePaths.push(filePath);
    } else {
      missingFiles.push(fileNames as string);
    }
  }

  // 4. 检查是否有文件不存在
  if (missingFiles.length > 0) {
    const isPartialMissing = isMultiple && filePaths.length > 0;

    // 单文件上传：保持原有错误格式
    // 多文件上传：使用新的增强格式
    throwError({
      operation: 'uploadFileToField',
      target: isMultiple
        ? `选择器 "${selector}"`
        : `文件 "${missingFiles[0]}"`,
      expected: isMultiple
        ? `所有文件存在于 fixtures 目录`
        : `文件存在于 ${resolveFixturePath(missingFiles[0], config.fixturesDir)}`,
      actual: isMultiple
        ? `缺失文件: ${missingFiles.join(', ')}`
        : undefined,
      suggestion: isPartialMissing
        ? `以下文件不存在:\n    ${missingFiles.map(f => `  - ${f}`).join('\n')}\n  可用文件:\n    ${fileNames.filter(f => !missingFiles.includes(f)).map(f => `  - ${f}`).join('\n')}`
        : isMultiple
        ? `检查文件名是否正确，或确认文件已添加到 fixtures 目录\n  缺失文件: ${missingFiles.join(', ')}`
        : '检查文件名是否正确，或确认文件已添加到 fixtures 目录'
    });
  }

  console.debug(`[uploadFileToField] 解析文件路径: ${filePaths.join(', ')}`);

  // 5. 查找文件输入框并上传
  try {
    const fileInput = page.locator(selector);
    console.debug(`[uploadFileToField] 找到文件输入框，准备上传 ${fileCount} 个文件`);

    // 使用 setInputFiles API
    // 单文件：传入字符串；多文件：传入数组
    const inputFiles = isMultiple ? filePaths : filePaths[0];
    await fileInput.setInputFiles(inputFiles, { timeout: config.timeout });
    console.debug(`[uploadFileToField] setInputFiles 完成`);

    // 6. 等待上传完成（如果需要）
    if (config.waitForUpload) {
      // 等待一小段时间确保文件上传处理完成
      await page.waitForTimeout(200);
      console.debug(`[uploadFileToField] 上传等待完成`);
    }

    console.debug(`[uploadFileToField] 上传完成 (${fileCount} 个文件)`);
  } catch (error) {
    // 选择器无效或其他错误 - 提供详细的上下文信息
    const errorMessage = error instanceof Error ? error.message : '未知错误';

    throwError({
      operation: 'uploadFileToField',
      target: `选择器 "${selector}"`,
      expected: '文件输入框存在于页面且可访问',
      actual: `错误: ${errorMessage}`,
      suggestion: [
        '检查选择器是否正确（推荐使用 data-testid）',
        '确认文件输入框已渲染到页面',
        '确认元素可见且未被隐藏（display: none）',
        '检查是否需要等待页面加载完成',
        isMultiple ? '确认 input 元素有 multiple 属性支持多文件上传' : ''
      ].filter(Boolean).join('\n        ')
    });
  }
}

/**
 * 解析 fixtures 文件路径
 *
 * @description
 * 将相对文件名解析为绝对路径。
 * 支持相对于 fixtures 目录的路径和子目录。
 *
 * @internal
 *
 * @param fileName - 文件名（可能包含子目录）
 * @param fixturesDir - fixtures 基础目录
 * @returns 解析后的绝对路径
 */

function resolveFixturePath(fileName: string, fixturesDir: string): string {
  const normalizedFileName = path.normalize(fileName);

  // 拒绝绝对路径和向上遍历路径
  if (normalizedFileName.startsWith("..") || path.isAbsolute(normalizedFileName)) {
    throwError({
      operation: "uploadFileToField",
      target: fileName,
      suggestion: "文件名必须是相对于 fixtures 目录的路径，不能使用 '..' 或绝对路径"
    });
  }

  // 解析完整路径
  const resolvedPath = path.resolve(path.join(fixturesDir, normalizedFileName));
  const resolvedFixturesDir = path.resolve(fixturesDir);

  // 验证解析后的路径在 fixtures 目录内（防止路径遍历攻击）
  if (!resolvedPath.startsWith(resolvedFixturesDir)) {
    throwError({
      operation: "uploadFileToField",
      target: fileName,
      suggestion: "文件名路径试图访问 fixtures 目录之外的文件"
    });
  }

  return resolvedPath;
}
