// TypeScript test file parser.
// Port of Rust implementation from oxc/tasks/coverage/src/typescript/meta.rs
// Generated by GitHub Copilot initially and amended since.
//
// This file is duplicated in `oxc-project/estree-conformance` repo in `src/utils` directory.
// Any changes made here also need to be replicated there.

import fs from "fs";
import path from "path";

// Regex patterns equivalent to the Rust version
const META_OPTIONS_REGEX = /^\/\/\s*@(\w+)\s*:\s*([^\r\n]*)/gm;

interface CompilerSettings {
  modules: string[];
  targets: string[];
  strict: boolean;
  jsx: string[];
  declaration: boolean;
  emitDeclarationOnly: boolean;
  alwaysStrict: boolean;
  allowUnreachableCode: boolean;
  allowUnusedLabels: boolean;
  noFallthroughCasesInSwitch: boolean;
}

interface SourceType {
  typescript: boolean;
  jsx: boolean;
  module: boolean;
}

interface TestUnitData {
  name: string;
  content: string;
  sourceType: SourceType;
}

interface TestCaseContent {
  tests: TestUnitData[];
  settings: CompilerSettings;
  errorFiles: string[];
}

/**
 * Convert settings value to boolean
 * @param value - Setting value
 * @param defaultValue - Default value if setting is not present
 */
function valueToBoolean(value: string | null, defaultValue: boolean): boolean {
  if (value === "true") return true;
  if (value === "false") return false;
  return defaultValue;
}

/**
 * Split comma-separated values into array
 * @param value - Setting value
 */
function splitValueOptions(value: string | null): string[] {
  if (!value) return [];
  return value.split(",").map((s) => s.trim().toLowerCase());
}

/**
 * Create compiler settings from options map
 * @param options - Compiler options
 * @returns Compiler settings
 */
function createCompilerSettings(options: Map<string, string>): CompilerSettings {
  return {
    modules: splitValueOptions(options.get("module")),
    targets: splitValueOptions(options.get("target")),
    strict: valueToBoolean(options.get("strict"), false),
    jsx: splitValueOptions(options.get("jsx")),
    declaration: valueToBoolean(options.get("declaration"), false),
    emitDeclarationOnly: valueToBoolean(options.get("emitdeclarationonly"), false),
    alwaysStrict: valueToBoolean(options.get("alwaysstrict"), false),
    allowUnreachableCode: valueToBoolean(options.get("allowunreachablecode"), true),
    allowUnusedLabels: valueToBoolean(options.get("allowunusedlabels"), true),
    noFallthroughCasesInSwitch: valueToBoolean(options.get("nofallthroughcasesinswitch"), false),
  };
}

// Mapping from file extension to source type
const EXTENSIONS = {
  ".js": { typescript: false, jsx: false, module: true },
  ".mjs": { typescript: false, jsx: false, module: true },
  ".cjs": { typescript: false, jsx: false, module: false },
  ".jsx": { typescript: false, jsx: true, module: true },
  ".ts": { typescript: true, jsx: false, module: true },
  ".mts": { typescript: true, jsx: false, module: true },
  ".cts": { typescript: true, jsx: false, module: false },
  ".tsx": { typescript: true, jsx: true, module: true },
};

function isSupportedExtension(ext: string): ext is keyof typeof EXTENSIONS {
  return Object.hasOwn(EXTENSIONS, ext);
}

/**
 * Check if extension explicitly indicates module type
 * @param ext - File extension (lowercase, with dot)
 */
function isExplicitModuleExtension(ext: string): boolean {
  return [".mts", ".mjs", ".cjs", ".cts"].includes(ext);
}

/**
 * Get source type from file path
 * @param filePath - Path to the file
 * @param options - Compiler options
 * @returns Source type, or `null` for an unsupported extension
 */
function getSourceType(filePath: string, options: CompilerSettings): SourceType | null {
  const ext = path.extname(filePath).toLowerCase();
  if (!isSupportedExtension(ext)) return null;

  // For explicit module extensions (.mts, .mjs, .cjs, .cts), preserve the extension-based module value.
  // For ambiguous extensions (.js, .jsx, .ts, .tsx), use content-based detection (start with false).
  const sourceType: SourceType = {
    ...EXTENSIONS[ext],
    module: isExplicitModuleExtension(ext) ? EXTENSIONS[ext].module : false,
  };
  if (options.jsx.length > 0) sourceType.jsx = true;

  return sourceType;
}

/**
 * Get error files for the test
 * @param filePath - Path to the test file
 * @param options - Compiler options
 * @returns Error files content
 */
function getErrorFiles(filePath: string, options: CompilerSettings): string[] {
  const fileName = path.basename(filePath, path.extname(filePath));
  const root = path.join(process.cwd(), "typescript/tests/baselines/reference");

  const suffixes = [
    "",
    ...options.modules.map((module) => `(module=${module})`),
    ...options.targets.map((target) => `(target=${target})`),
    ...options.jsx.map((jsx) => `(jsx=${jsx})`),
  ];

  const errorFiles = [];

  for (const suffix of suffixes) {
    const errorPath = path.join(root, `${fileName}${suffix}.errors.txt`);
    try {
      if (fs.existsSync(errorPath)) {
        const errorFile = fs.readFileSync(errorPath, "utf8");
        errorFiles.push(errorFile);
      }
    } catch {
      // Skip if file doesn't exist or can't be read
    }
  }

  return errorFiles;
}

/**
 * Extract individual test units from a TypeScript test file
 * @param filePath - Path to the test file
 * @param code - Content of the test file
 * @returns Parsed test case content
 */
export function makeUnitsFromTest(filePath: string, code: string): TestCaseContent {
  const currentFileOptions = new Map<string, string>();
  let currentFileName: string | null = null;
  const testUnitData: Omit<TestUnitData, "sourceType">[] = [];
  let currentFileContent = "";

  // Process the file line by line
  const lines = code.split(/\r?\n/);
  if (lines[lines.length - 1] === "") lines.length--;

  for (const line of lines) {
    // Reset regex state
    META_OPTIONS_REGEX.lastIndex = 0;
    const match = META_OPTIONS_REGEX.exec(line);

    if (match) {
      const metaName = match[1].toLowerCase();
      const metaValue = match[2].trim();

      if (metaName === "filename") {
        if (currentFileName !== null) {
          testUnitData.push({
            name: currentFileName,
            content: currentFileContent,
          });
          currentFileContent = "";
        }
        currentFileName = metaValue;
      } else {
        currentFileOptions.set(metaName, metaValue);
      }
    } else {
      if (currentFileContent.length > 0) {
        currentFileContent += "\n";
      }
      currentFileContent += line;
    }
  }

  // Handle the last file (or the only file if no @filename is used)
  const fileName = currentFileName || path.basename(filePath);
  testUnitData.push({
    name: fileName,
    content: currentFileContent,
  });

  const settings = createCompilerSettings(currentFileOptions);

  // Update source types and filter out unsupported files
  const isModule = testUnitData.length > 1;
  const validTestUnits: TestUnitData[] = [];
  for (const unit of testUnitData) {
    const sourceType = getSourceType(unit.name, settings);
    if (!sourceType) continue;
    // Only upgrade to module for ambiguous extensions (.ts, .js, .tsx, .jsx).
    // Explicit extensions (.mts, .mjs, .cjs, .cts) should keep their extension-based module kind.
    const ext = path.extname(unit.name).toLowerCase();
    if (isModule && !isExplicitModuleExtension(ext)) sourceType.module = true;
    validTestUnits.push({ ...unit, sourceType });
  }

  const errorFiles = getErrorFiles(filePath, settings);

  return {
    tests: validTestUnits,
    settings,
    errorFiles,
  };
}
