import type { SpawnSyncReturns } from 'node:child_process';

import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import {
  chmodSync,
  copyFileSync,
  cpSync,
  existsSync,
  mkdirSync,
  mkdtempSync,
  readdirSync,
  readFileSync,
  realpathSync,
  rmSync,
  statSync,
  writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, relative, resolve } from 'node:path';
import process from 'node:process';

import { build } from 'vite';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

const root = resolve(__dirname, '..'); // eslint-disable-line unicorn/prefer-module
const temporaryRoot = mkdtempSync(join(tmpdir(), 'eln-template-filler-package-'));
const outputDirectory = join(temporaryRoot, 'dist');
const pluginPackage = join(outputDirectory, 'eln-template-filler.plugin');
const scriptSandbox = join(temporaryRoot, 'script-sandbox');
const packageScript = join(scriptSandbox, 'scripts/package.sh');
const dockerStubDirectory = join(temporaryRoot, 'docker-stub');
const dockerLog = join(temporaryRoot, 'docker.log');

interface Manifest {
  guid: string;
  variations: Array<{ icons: string[]; url: string }>;
  version: string;
}

function listFiles(directory: string): string[] {
  return readdirSync(directory)
    .flatMap((name) => {
      const absolutePath = join(directory, name);
      return statSync(absolutePath).isDirectory() ? listFiles(absolutePath) : [relative(outputDirectory, absolutePath)];
    })
    .sort();
}

function listDirectoryFiles(directory: string, base = directory): string[] {
  return readdirSync(directory)
    .flatMap((name) => {
      const absolutePath = join(directory, name);
      return statSync(absolutePath).isDirectory() ? listDirectoryFiles(absolutePath, base) : [relative(base, absolutePath)];
    })
    .sort();
}

function readOutput(relativePath: string): string {
  return readFileSync(join(outputDirectory, relativePath), 'utf8');
}

function runScript(script: string, args: string[] = [], environment: Record<string, string> = {}, cwd = root): SpawnSyncReturns<string> {
  return spawnSync('sh', [script, ...args], {
    cwd,
    encoding: 'utf8',
    env: { ...process.env, ...environment },
  });
}

function runPackage(environment: Record<string, string> = {}, cwd = root): SpawnSyncReturns<string> {
  return runScript(
    packageScript,
    [],
    {
      ONLYOFFICE_PLUGIN_DIST_DIR: outputDirectory,
      ...environment,
    },
    cwd,
  );
}

function archiveEntries(archive: string): string[] {
  const result = spawnSync('unzip', ['-Z1', archive], { encoding: 'utf8' });
  expect(result.status, result.stderr).toBe(0);
  return result.stdout.trim().split('\n').filter(Boolean);
}

function readArchiveEntry(archive: string, entry: string): string {
  const result = spawnSync('unzip', ['-p', archive, entry], { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
  expect(result.status, result.stderr).toBe(0);
  return result.stdout;
}

function fileHash(file: string): string {
  return createHash('sha256').update(readFileSync(file)).digest('hex');
}

function writeExecutable(file: string, source: string): void {
  writeFileSync(file, source);
  chmodSync(file, 0o755);
}

function resetDockerLog(): void {
  rmSync(dockerLog, { force: true });
}

function readDockerLog(): string {
  return existsSync(dockerLog) ? readFileSync(dockerLog, 'utf8') : '';
}

function installEnvironment(pluginPackage: string, pluginExists = '0'): Record<string, string> {
  return {
    DOCKER_LOG: dockerLog,
    DOCKER_PLUGIN_EXISTS: pluginExists,
    ONLYOFFICE_PLUGIN_PACKAGE: pluginPackage,
    PATH: `${dockerStubDirectory}:${process.env.PATH ?? ''}`,
  };
}

function createInstallArchive(
  name: string,
  options: { change?: (manifest: Manifest) => void; malformedManifest?: boolean; omittedEntry?: string } = {},
): string {
  const directory = mkdtempSync(join(temporaryRoot, `${name}-`));
  const archive = join(directory, `${name}.plugin`);
  const manifest = JSON.parse(readFileSync(join(root, 'config.json'), 'utf8')) as Manifest;
  manifest.variations[0]!.url = 'index.html?v=0.1.0';
  options.change?.(manifest);
  writeFileSync(join(directory, 'config.json'), options.malformedManifest ? '{invalid json' : `${JSON.stringify(manifest)}\n`);

  for (const entry of ['index.html', ...manifest.variations[0]!.icons, 'translations/langs.json']) {
    const absolutePath = join(directory, entry);
    mkdirSync(dirname(absolutePath), { recursive: true });
    if (entry === options.omittedEntry) {
      mkdirSync(absolutePath, { recursive: true });
      writeFileSync(join(absolutePath, 'not-the-required-file'), 'directory impostor');
      writeFileSync(`${absolutePath}.bak`, 'substring impostor');
    } else {
      writeFileSync(absolutePath, entry === 'translations/langs.json' ? '[]\n' : `fixture for ${entry}`);
    }
  }

  const entries = readdirSync(directory);
  const zipped = spawnSync('zip', ['-q', '-r', archive, ...entries], { cwd: directory, encoding: 'utf8' });
  expect(zipped.status, zipped.stderr).toBe(0);
  return archive;
}

function createManifestArchive(name: string, change: (manifest: Manifest) => void): string {
  return createInstallArchive(name, { change });
}

beforeAll(async () => {
  await build({
    root,
    configFile: join(root, 'vite.config.ts'),
    build: {
      emptyOutDir: true,
      outDir: outputDirectory,
    },
  });

  mkdirSync(join(scriptSandbox, 'scripts'), { recursive: true });
  copyFileSync(join(root, 'scripts/package.sh'), packageScript);
  copyFileSync(join(root, 'scripts/production-contract.mjs'), join(scriptSandbox, 'scripts/production-contract.mjs'));
  chmodSync(packageScript, 0o755);

  mkdirSync(dockerStubDirectory, { recursive: true });
  writeExecutable(
    join(dockerStubDirectory, 'docker'),
    `#!/usr/bin/env sh
set -eu
{
  printf '['
  for argument in "$@"; do
    printf '<%s>' "$argument"
  done
  printf ']\\n'
} >> "$DOCKER_LOG"
case "$*" in
  *' test -f '*) exit "\${DOCKER_PLUGIN_EXISTS:-0}" ;;
esac
if [ "\${1:-}" = 'cp' ]; then
  exit "\${DOCKER_CP_STATUS:-0}"
fi
exit 0
`,
  );
}, 30_000);

afterAll(() => {
  rmSync(temporaryRoot, { force: true, recursive: true });
});

describe('m1 正式构建产物', () => {
  it('保留正式身份、版本和入口', () => {
    const manifest = JSON.parse(readOutput('config.json')) as {
      guid: string;
      minVersion: string;
      name: string;
      nameLocale: Record<string, string>;
      variations: Array<{ isInsideMode: boolean; isModal: boolean; size?: number[]; url: string }>;
      version: string;
    };

    expect(manifest).toMatchObject({
      guid: 'asc.{7F4E8F35-5D66-47F8-A5B4-6AB3FAACF565}',
      minVersion: '9.4.0',
      name: 'ELN 实验模板填写',
      nameLocale: { 'zh-ZH': 'ELN 实验模板填写' },
      version: '0.1.0',
    });
    expect(manifest.variations[0]).toMatchObject({
      isInsideMode: true,
      isModal: false,
      url: 'index.html?v=0.1.0',
    });
    expect(manifest.variations[0]).not.toHaveProperty('size');
  });

  it('只输出正式入口、manifest、哈希资源和固定路径图标', () => {
    const files = listFiles(outputDirectory);

    expect(files).toContain('config.json');
    expect(files).toContain('index.html');
    expect(files).not.toContain('vue-index.html');
    expect(files).toContain('resources/icon.png');
    expect(files).toContain('resources/icon@2x.png');
    expect(files).toContain('translations/langs.json');
    expect(files).not.toContain('resources/icon-v2.png');
    expect(files).not.toContain('resources/icon-v2@2x.png');
    expect(
      files.some((file) =>
        /^assets\/.(?:[^-\n\r\u2028\u2029]*-[^-\w\n\r\u2028\u2029])*[^-\n\r\u2028\u2029]*-[-\w]+(?:[^-\w\n\r\u2028\u2029](?:[^-\n\r\u2028\u2029]*-[^-\w\n\r\u2028\u2029])*[^-\n\r\u2028\u2029]*-[-\w]+)*\.js$/.test(
          file,
        ),
      ),
    ).toBe(true);
    expect(
      files.some((file) =>
        /^assets\/.(?:[^-\n\r\u2028\u2029]*-[^-\w\n\r\u2028\u2029])*[^-\n\r\u2028\u2029]*-[-\w]+(?:[^-\w\n\r\u2028\u2029](?:[^-\n\r\u2028\u2029]*-[^-\w\n\r\u2028\u2029])*[^-\n\r\u2028\u2029]*-[-\w]+)*\.css$/.test(
          file,
        ),
      ),
    ).toBe(true);
    expect(files.some((file) => file.endsWith('.map'))).toBe(false);

    for (const legacyFile of ['plugin.js', 'field-types.js', 'office-commands.js']) {
      expect(files).not.toContain(legacyFile);
    }
  });

  it('使用相对哈希资源并保留相对 ONLYOFFICE SDK 地址', () => {
    const html = readOutput('index.html');

    expect(html).toMatch(/(?:src|href)="\.\/assets\//);
    expect(html).not.toMatch(/(?:src|href)="\/assets\//);
    expect(html).toContain('href="../v1/plugins.css"');
    expect(html).toContain('src="../v1/plugins.js"');
  });

  it('bundle 保留两个自包含 command 的产物级调用标识', () => {
    const javascript = listFiles(outputDirectory)
      .filter((file) => file.endsWith('.js'))
      .map((file) => readOutput(file))
      .join('\n');

    expect(javascript).toContain('callCommand');
    expect(javascript).toContain('elnTemplateFillInspect');
    expect(javascript).toContain('elnTemplateFillChanges');
  });

  it('按高置信规则审计临时构建与最终 zip，且只放行 Ant Design 内部标识', () => {
    const packaged = runPackage();
    expect(packaged.status, packaged.stderr).toBe(0);
    const outputFiles = listFiles(outputDirectory);
    const zipFiles = archiveEntries(pluginPackage);
    const textFiles = outputFiles.filter((file) => /\.(?:css|html|js|json)$/.test(file));
    const buildText = textFiles.map((file) => readOutput(file)).join('\n');
    const zipTextFiles = zipFiles.filter((file) => /\.(?:css|html|js|json)$/.test(file));
    const zipText = zipTextFiles.map((file) => readArchiveEntry(pluginPackage, file)).join('\n');

    expect(zipTextFiles.sort()).toEqual(textFiles.sort());
    for (const textOutput of [buildText, zipText]) {
      const antDLocalhostContexts = textOutput.match(
        /[A-Za-z_$][\w$]*="\(\?:"\+[A-Za-z_$][\w$]*\+"\|www\\\\\.\)"\+[A-Za-z_$][\w$]*\+"\(\?:localhost\|"\+[A-Za-z_$][\w$]*\+"\|"\+[A-Za-z_$][\w$]*\+"\|"\+[A-Za-z_$][\w$]*\+[A-Za-z_$][\w$]*\+[A-Za-z_$][\w$]*\+"\)"\+[A-Za-z_$][\w$]*\+[A-Za-z_$][\w$]*/g,
      );
      const antDSecret = 'SECRET_COMBOBOX_MODE_DO_NOT_USE';
      const antDSecretContexts = [
        new RegExp(`[A-Za-z_$][\\w$]*="${antDSecret}",[A-Za-z_$][\\w$]*=[A-Za-z_$][\\w$]*\\(`, 'g'),
        new RegExp(`,${antDSecret}:[A-Za-z_$][\\w$]*,slots:Object`, 'g'),
        new RegExp(`\\.${antDSecret},getInputElement:`, 'g'),
      ].flatMap((pattern) => textOutput.match(pattern) ?? []);
      expect(antDLocalhostContexts).toHaveLength(1);
      expect(antDSecretContexts).toHaveLength(3);
      expect(textOutput.split(antDSecret)).toHaveLength(4);
      const auditedText = [antDLocalhostContexts![0]!, ...antDSecretContexts].reduce((text, context) => text.replace(context, ''), textOutput);
      expect(auditedText).not.toMatch(/\b[A-Z0-9_]*SECRET[A-Z0-9_]*\b/);
      expect(auditedText).not.toMatch(/ONLYOFFICE_JWT_SECRET|-----BEGIN [A-Z ]*PRIVATE KEY-----/);
      expect(auditedText).not.toMatch(
        /\b(?:localhost|127\.0\.0\.1|0\.0\.0\.0)\b|https?:\/\/(?:10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+)/i,
      );
      expect(auditedText).not.toMatch(/客户数据样例|客户样例|customer[-_ ]?(?:data|sample)/i);
    }
    expect([...outputFiles, ...zipFiles]).not.toEqual(expect.arrayContaining([expect.stringMatching(/\.(?:docx?|pdf)$/i)]));
    expect([...outputFiles, ...zipFiles]).not.toEqual(expect.arrayContaining([expect.stringMatching(/(?:截图|screenshot).*\.(?:png|jpe?g)$/i)]));
    expect([...outputFiles, ...zipFiles]).not.toEqual(
      expect.arrayContaining([expect.stringMatching(/^(?:deploy\/nginx|vite\/onlyoffice-plugin\.ts|scripts\/release\.sh)/)]),
    );
  });

  it.each(['ONLYOFFICE_PLUGIN_DEV_PORT', 'ONLYOFFICE_DOCS_URL'])('拒绝开发环境变量标识 %s 进入正式产物', (marker) => {
    const sandbox = mkdtempSync(join(temporaryRoot, 'development-marker-'));
    cpSync(outputDirectory, sandbox, { recursive: true });
    const javascript = listDirectoryFiles(sandbox).find((file) => file.endsWith('.js'));
    expect(javascript).toBeDefined();
    const asset = join(sandbox, javascript!);
    writeFileSync(asset, `${readFileSync(asset, 'utf8')}\n${marker}\n`);

    const validated = spawnSync(process.execPath, [join(root, 'scripts/production-contract.mjs'), 'validate-dist', sandbox], {
      encoding: 'utf8',
    });

    expect(validated.status).not.toBe(0);
  });

  it('不写回正式源码 manifest', () => {
    const sourceManifest = JSON.parse(readFileSync(join(root, 'config.json'), 'utf8')) as {
      variations: Array<{ url: string }>;
      version: string;
    };

    expect(sourceManifest.version).toBe('0.1.0');
    expect(sourceManifest.variations[0]?.url).toBe('index.html?v=0.1.0');
  });
});

describe('正式打包与安装脚本', () => {
  it('覆盖替换后的目标目录文件集合严格等于安装包清单', () => {
    const packaged = runPackage();
    expect(packaged.status, packaged.stderr).toBe(0);
    const targetDirectory = join(temporaryRoot, 'installed-plugin');
    const installDirectory = join(temporaryRoot, '.installed-plugin.install');
    const backupDirectory = join(temporaryRoot, '.installed-plugin.backup');
    mkdirSync(join(targetDirectory, 'assets'), { recursive: true });
    writeFileSync(join(targetDirectory, 'vue-index.html'), 'stale candidate');
    writeFileSync(join(targetDirectory, 'assets/old-hash.js'), 'stale hash');

    const replaced = runScript(join(root, 'scripts/replace-plugin-directory.sh'), [pluginPackage, targetDirectory, installDirectory, backupDirectory]);

    expect(replaced.status, replaced.stderr).toBe(0);
    expect(listDirectoryFiles(targetDirectory)).toEqual(
      archiveEntries(pluginPackage)
        .filter((entry) => !entry.endsWith('/'))
        .sort(),
    );
    expect(existsSync(installDirectory)).toBe(false);
    expect(existsSync(backupDirectory)).toBe(false);
  });

  it('staging 解压失败时保留原目录、清理临时目录并返回原退出码', () => {
    const targetDirectory = join(temporaryRoot, 'failed-installed-plugin');
    const installDirectory = join(temporaryRoot, '.failed-installed-plugin.install');
    const backupDirectory = join(temporaryRoot, '.failed-installed-plugin.backup');
    const unzipStubDirectory = join(temporaryRoot, 'failing-unzip');
    const archive = join(temporaryRoot, 'unused.plugin');
    mkdirSync(targetDirectory, { recursive: true });
    mkdirSync(unzipStubDirectory, { recursive: true });
    writeFileSync(join(targetDirectory, 'keep.txt'), 'original');
    writeFileSync(archive, 'stub archive');
    writeExecutable(join(unzipStubDirectory, 'unzip'), '#!/usr/bin/env sh\nexit 37\n');

    const failed = runScript(join(root, 'scripts/replace-plugin-directory.sh'), [archive, targetDirectory, installDirectory, backupDirectory], {
      PATH: `${unzipStubDirectory}:${process.env.PATH ?? ''}`,
    });

    expect(failed.status).toBe(37);
    expect(readFileSync(join(targetDirectory, 'keep.txt'), 'utf8')).toBe('original');
    expect(existsSync(installDirectory)).toBe(false);
    expect(existsSync(backupDirectory)).toBe(false);
  });

  it('在覆盖 dist 上真实生成 CRC 有效且不自包含的正式包', () => {
    const packaged = runPackage();
    expect(packaged.status, packaged.stderr).toBe(0);
    expect(packaged.stdout.trim()).toBe(pluginPackage);

    const integrity = spawnSync('unzip', ['-tq', pluginPackage], { encoding: 'utf8' });
    expect(integrity.status, integrity.stderr).toBe(0);
    expect(archiveEntries(pluginPackage).sort()).toEqual(
      [
        'assets/',
        ...listFiles(outputDirectory).filter((file) => file.startsWith('assets/')),
        'config.json',
        'resources/',
        'resources/icon.png',
        'resources/icon@2x.png',
        'translations/',
        'translations/langs.json',
        'index.html',
      ].sort(),
    );
    expect(archiveEntries(pluginPackage)).not.toContain('eln-template-filler.plugin');
  });

  it('支持相对的 ONLYOFFICE_PLUGIN_DIST_DIR', () => {
    const packaged = runPackage({ ONLYOFFICE_PLUGIN_DIST_DIR: 'dist' }, temporaryRoot);

    expect(packaged.status, packaged.stderr).toBe(0);
    expect(realpathSync(packaged.stdout.trim())).toBe(realpathSync(pluginPackage));
  });

  it.each([
    ['顶层额外文件', 'debug.txt'],
    ['assets 隐藏文件', 'assets/.hidden.js'],
    ['assets 子目录', 'assets/nested/extra.js'],
    ['resources 额外文件', 'resources/extra.png'],
  ])('拒绝外部 dist 中的%s', (_, extraPath) => {
    const externalDist = mkdtempSync(join(temporaryRoot, 'external-dist-'));
    cpSync(outputDirectory, externalDist, { recursive: true });
    rmSync(join(externalDist, 'eln-template-filler.plugin'), { force: true });
    const extra = join(externalDist, extraPath);
    mkdirSync(dirname(extra), { recursive: true });
    writeFileSync(extra, 'unexpected');

    const packaged = runPackage({ ONLYOFFICE_PLUGIN_DIST_DIR: externalDist });

    expect(packaged.status).not.toBe(0);
    expect(existsSync(join(externalDist, 'eln-template-filler.plugin'))).toBe(false);
  });

  it('zip 失败时保留旧包哈希并清理唯一临时文件', () => {
    const seeded = runPackage();
    expect(seeded.status, seeded.stderr).toBe(0);
    const previousHash = fileHash(pluginPackage);
    const zipStubDirectory = join(temporaryRoot, 'failing-zip');
    const zipLog = join(temporaryRoot, 'zip.log');
    mkdirSync(zipStubDirectory, { recursive: true });
    writeExecutable(
      join(zipStubDirectory, 'zip'),
      `#!/usr/bin/env sh
printf '%s\\n' "$*" > "$ZIP_STUB_LOG"
exit 29
`,
    );

    const failed = runPackage({
      PATH: `${zipStubDirectory}:${process.env.PATH ?? ''}`,
      ZIP_STUB_LOG: zipLog,
    });

    expect(failed.status).not.toBe(0);
    expect(readFileSync(zipLog, 'utf8')).toMatch(/\.eln-template-filler\.\d+\.plugin/);
    expect(fileHash(pluginPackage)).toBe(previousHash);
    expect(readdirSync(outputDirectory).filter((file) => /^\.eln-template-filler\..+\.plugin$/.test(file))).toEqual([]);
  });

  it('从 dist 显式打包并以校验后的临时包原子替换', () => {
    const scriptPath = join(root, 'scripts/package.sh');
    expect(existsSync(scriptPath)).toBe(true);
    const script = readFileSync(scriptPath, 'utf8');

    expect(script).toMatch(/^#!\/usr\/bin\/env sh\nset -eu/);
    expect(script).toContain('jq -e');
    expect(script).toContain('"$DIST_DIR/config.json"');
    expect(script).toContain('index.html');
    expect(script).not.toContain('vue-index.html');
    expect(script).toContain('assets');
    expect(script).toContain('resources/icon.png');
    expect(script).toContain('resources/icon@2x.png');
    expect(script).toContain('translations/langs.json');
    expect(script).not.toContain('rm -f "$PACKAGE"');
    expect(script).toMatch(/cd "\$DIST_DIR"/);
    expect(script).toContain('production-contract.mjs');
    expect(script).toContain('validate-dist');
    expect(script).not.toMatch(/\bfind\b|\bgrep\b/);
    expect(script).toContain('ONLYOFFICE_PLUGIN_DIST_DIR');
    expect(script).toContain('unzip -tq "$TEMP_PACKAGE"');
    expect(script).toContain('mv -f "$TEMP_PACKAGE" "$PACKAGE"');
    expect(script).not.toContain('zip -q -r "$PACKAGE" .');
    expect(script).not.toContain('zip -q -r');
  });

  it('仅在显式提供绝对包路径时绕过正式 package.sh', () => {
    const script = readFileSync(join(root, 'scripts/install-docker.sh'), 'utf8');

    expect(script).toContain('ONLYOFFICE_PLUGIN_PACKAGE');
    expect(script).toContain('replace-plugin-directory.sh');
    expect(script).toMatch(/case "\$ONLYOFFICE_PLUGIN_PACKAGE" in/);
    expect(script).toMatch(/\/\*\)/);
    expect(script).toMatch(/\[ ! -f "\$ONLYOFFICE_PLUGIN_PACKAGE" \]/);
    expect(script).toContain('PACKAGE=$ONLYOFFICE_PLUGIN_PACKAGE');
    expect(script).toContain('PACKAGE=$("$SCRIPT_DIR/package.sh")');
    expect(script).not.toContain('eval ');
    expect(script).toContain('docker cp "$PACKAGE" "$CONTAINER:$CONTAINER_PACKAGE"');
  });

  it.each([
    ['覆盖', '0', '<sh></tmp/replace-plugin-directory.', 'pluginsmanager>'],
    ['首次', '1', 'pluginsmanager>', '<sh></tmp/replace-plugin-directory.'],
  ])('正式包带空格路径通过预检后走%s安装并在退出时清理唯一临时包', (_label, pluginExists, expected, unexpected) => {
    const packaged = runPackage();
    expect(packaged.status, packaged.stderr).toBe(0);
    const spacedDirectory = join(temporaryRoot, 'plugin packages');
    const spacedPackage = join(spacedDirectory, 'formal plugin.plugin');
    mkdirSync(spacedDirectory, { recursive: true });
    copyFileSync(pluginPackage, spacedPackage);
    resetDockerLog();

    const installed = runScript(join(root, 'scripts/install-docker.sh'), ['test onlyoffice'], installEnvironment(spacedPackage, pluginExists));
    const log = readDockerLog();

    expect(installed.status, installed.stderr).toBe(0);
    expect(log).toContain(`<cp><${spacedPackage}><test onlyoffice:/tmp/eln-template-filler.plugin.`);
    expect(log).toContain(expected);
    expect(log).not.toContain(unexpected);
    expect(log).toMatch(/<find><\/var\/www\/onlyoffice\/documentserver\/sdkjs-plugins\/\{7F4E8F35-5D66-47F8-A5B4-6AB3FAACF565\}>.*<\*\.gz><-delete>/);
    expect(log).toMatch(/<exec><test onlyoffice><rm><-f><\/tmp\/eln-template-filler\.plugin\.\d+>/);
  });

  it('manifest 入口去除 query 后精确匹配包内 HTML', () => {
    const pluginPackage = createInstallArchive('query-url', {
      change: (manifest) => {
        manifest.variations[0]!.url = 'index.html?cache=abc&v=0.1.0';
      },
    });
    resetDockerLog();

    const installed = runScript(join(root, 'scripts/install-docker.sh'), [], installEnvironment(pluginPackage));

    expect(installed.status, installed.stderr).toBe(0);
    expect(readDockerLog()).toContain('<cp>');
  });

  it.each(['index.html', 'resources/icon.png', 'resources/icon@2x.png', 'translations/langs.json'])(
    '%s 缺失时同名目录和 substring 条目不能通过预检',
    (omittedEntry) => {
    const pluginPackage = createInstallArchive(`missing-${omittedEntry.replaceAll('/', '-')}`, { omittedEntry });
    resetDockerLog();

    const installed = runScript(join(root, 'scripts/install-docker.sh'), [], installEnvironment(pluginPackage));

    expect(installed.status).not.toBe(0);
    expect(readDockerLog()).toBe('');
    },
  );

  it('cRC 正常但 config.json 无法由 jq 解析时在 Docker 前拒绝', () => {
    const pluginPackage = createInstallArchive('malformed-manifest', { malformedManifest: true });
    resetDockerLog();

    const installed = runScript(join(root, 'scripts/install-docker.sh'), [], installEnvironment(pluginPackage));

    expect(installed.status).not.toBe(0);
    expect(readDockerLog()).toBe('');
  });

  it('docker cp 失败仍清理唯一容器路径并保留原退出码', () => {
    const packaged = runPackage();
    expect(packaged.status, packaged.stderr).toBe(0);
    resetDockerLog();

    const installed = runScript(join(root, 'scripts/install-docker.sh'), [], {
      ...installEnvironment(pluginPackage),
      DOCKER_CP_STATUS: '37',
    });
    const log = readDockerLog();

    expect(installed.status).toBe(37);
    expect(log).toContain(`<cp><${pluginPackage}>`);
    expect(log).toMatch(/<exec><cx-onlyoffice><rm><-f><\/tmp\/eln-template-filler\.plugin\.\d+>/);
    expect(log).not.toContain('pluginsmanager>');
    expect(log).not.toContain('<unzip><-q><-o>');
  });

  it.each([
    [
      '损坏 zip',
      () => {
        const file = join(temporaryRoot, 'invalid plugin.plugin');
        writeFileSync(file, 'not a zip');
        return file;
      },
    ],
    [
      '错误 GUID',
      () =>
        createManifestArchive('wrong-guid', (manifest) => {
          manifest.guid = 'asc.{00000000-0000-0000-0000-000000000000}';
        }),
    ],
    [
      '错误入口',
      () =>
        createManifestArchive('wrong-url', (manifest) => {
          manifest.variations[0]!.url = 'unexpected.html?v=0.1.0';
        }),
    ],
  ] as const)('%s 在任何 Docker 调用前被拒绝', (_label, createPackage) => {
    resetDockerLog();
    const installed = runScript(join(root, 'scripts/install-docker.sh'), [], installEnvironment(createPackage()));

    expect(installed.status).not.toBe(0);
    expect(readDockerLog()).toBe('');
  });

  it('正式 package.sh 返回的损坏包同样在 Docker 前被拒绝', () => {
    const invalidPackage = join(temporaryRoot, 'formal-invalid.plugin');
    writeFileSync(invalidPackage, 'not a zip');
    const formalSandbox = join(temporaryRoot, 'formal-install', 'scripts');
    mkdirSync(formalSandbox, { recursive: true });
    copyFileSync(join(root, 'scripts/install-docker.sh'), join(formalSandbox, 'install-docker.sh'));
    writeExecutable(
      join(formalSandbox, 'package.sh'),
      `#!/usr/bin/env sh
printf '%s\\n' '${invalidPackage}'
`,
    );
    resetDockerLog();

    const installed = runScript(join(formalSandbox, 'install-docker.sh'), [], {
      DOCKER_LOG: dockerLog,
      DOCKER_PLUGIN_EXISTS: '0',
      PATH: `${dockerStubDirectory}:${process.env.PATH ?? ''}`,
    });

    expect(installed.status).not.toBe(0);
    expect(readDockerLog()).toBe('');
  });
});
