import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { join, relative } from "node:path";
import { parse as parseSfc } from "@vue/compiler-sfc";
import { baseParse, NodeTypes } from "@vue/compiler-dom";
import ts from "typescript";

const projectRoot = process.cwd();
const sourceRoot = join(projectRoot, "src");
const hasChinese = (value: string | undefined): boolean => /\p{Script=Han}/u.test(value ?? "");

const excludedVueFiles = new Set(
  [
    "src/pages/home/components/LongTextGuideCardPreview/guide.vue",
    "src/pages/home/components/LongTextGuideCardPreview/photo.vue",
    "src/pages/home/components/LongTextGuideCardPreview/poi.vue",
    "src/pages/home/components/LongTextGuideCardPreview/route.vue",
  ].map((item) => item.replace(/\//g, "\\")),
);

function collectVueFiles(dir: string, files: string[] = []): string[] {
  if (!existsSync(dir)) {
    return files;
  }

  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    const fullPath = join(dir, entry.name);
    if (entry.isDirectory()) {
      collectVueFiles(fullPath, files);
      continue;
    }

    if (entry.isFile() && entry.name.endsWith(".vue")) {
      files.push(fullPath);
    }
  }

  return files;
}

function getLine(source: string, offset: number): number {
  return source.slice(0, offset).split(/\r?\n/).length;
}

function collectTemplateFindings(file: string, source: string): string[] {
  const findings: string[] = [];
  const { descriptor } = parseSfc(source, { filename: file });
  const template = descriptor.template;
  if (!template) {
    return findings;
  }

  let ast;
  try {
    ast = baseParse(template.content);
  } catch {
    return findings;
  }

  function report(offset: number, text: string): void {
    findings.push(`${relative(projectRoot, file)}:${getLine(source, template.loc.start.offset + offset)} ${text}`);
  }

  function walk(node: any): void {
    if (node.type === NodeTypes.TEXT && hasChinese(node.content)) {
      report(node.loc.start.offset, node.content.trim());
    }

    if (node.type === NodeTypes.INTERPOLATION) {
      const expression = node.content?.loc?.source ?? "";
      if (hasChinese(expression)) {
        report(node.loc.start.offset, expression);
      }
    }

    if (node.type === NodeTypes.ELEMENT) {
      for (const prop of node.props ?? []) {
        if (prop.type === NodeTypes.ATTRIBUTE && hasChinese(prop.value?.content)) {
          report(prop.loc.start.offset, `${prop.name}="${prop.value?.content}"`);
        }

        if (prop.type === NodeTypes.DIRECTIVE && hasChinese(prop.exp?.loc?.source)) {
          report(prop.loc.start.offset, prop.exp?.loc?.source ?? "");
        }
      }
    }

    for (const child of node.children ?? []) {
      walk(child);
    }
  }

  walk(ast);
  return findings;
}

function collectScriptFindings(file: string, source: string): string[] {
  const findings: string[] = [];
  const { descriptor } = parseSfc(source, { filename: file });
  const blocks = [descriptor.script, descriptor.scriptSetup].filter(Boolean);
  const uiCallNames = new Set([
    "showToast",
    "showDialog",
    "showConfirmDialog",
    "showFailToast",
    "showSuccessToast",
    "showNotify",
    "showLoading",
    "showModal",
  ]);

  for (const block of blocks) {
    const blockContent = block?.content ?? "";
    const ast = ts.createSourceFile(`${file}.ts`, blockContent, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);

    function isUiCall(node: ts.CallExpression): boolean {
      const expression = node.expression;
      if (ts.isIdentifier(expression)) {
        return uiCallNames.has(expression.text);
      }

      if (ts.isPropertyAccessExpression(expression)) {
        return uiCallNames.has(expression.name.text);
      }

      return false;
    }

    function walk(node: ts.Node): void {
      if (ts.isCallExpression(node) && isUiCall(node)) {
        const text = node.getText(ast);
        if (hasChinese(text)) {
          const line = getLine(source, (block?.loc.start.offset ?? 0) + node.getStart(ast));
          findings.push(`${relative(projectRoot, file)}:${line} ${text}`);
        }
      }

      ts.forEachChild(node, walk);
    }

    walk(ast);
  }

  return findings;
}

describe("visible UI copy", () => {
  it("uses i18n keys instead of hardcoded Chinese strings", () => {
    const findings = collectVueFiles(sourceRoot)
      .filter((file) => !excludedVueFiles.has(relative(projectRoot, file)))
      .flatMap((file) => {
        const source = readFileSync(file, "utf8");
        return [...collectTemplateFindings(file, source), ...collectScriptFindings(file, source)];
      });

    assert.deepEqual(findings, []);
  });
});
