import assert from 'node:assert/strict';
import { execFileSync, spawnSync } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';

const root = path.resolve(import.meta.dirname, '..');
const audit = path.join(root, 'tools/audit-public-content.py');
const stage = path.join(root, 'tools/stage-web-content.mjs');

function zipTree(archive, directory) {
  execFileSync('zip', ['-q', '-X', '-r', archive, '.'], { cwd: directory });
}

function zipEntries(archive, entries) {
  const tree = fs.mkdtempSync(path.join(path.dirname(archive), '.pack-tree-'));
  try {
    for (const [name, contents] of Object.entries(entries)) {
      const destination = path.join(tree, ...name.split('/'));
      fs.mkdirSync(path.dirname(destination), { recursive: true });
      fs.writeFileSync(destination, contents);
    }
    zipTree(archive, tree);
  } finally {
    fs.rmSync(tree, { recursive: true, force: true });
  }
}

function fixture(t) {
  const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'q4-content-license-'));
  t.after(() => fs.rmSync(temp, { recursive: true, force: true }));
  for (const name of ['public', 'retail', 'public-tree', 'retail-tree']) {
    fs.mkdirSync(path.join(temp, name));
  }
  const allowlist = path.join(temp, 'allowlist.json');
  fs.writeFileSync(allowlist, '{"schema":1,"entries":[]}\n');
  return { temp, allowlist };
}

function runAudit({ temp, allowlist }) {
  return spawnSync('python3', [
    audit,
    '--public-dir', path.join(temp, 'public'),
    '--retail-dir', path.join(temp, 'retail'),
    '--allowlist', allowlist
  ], { encoding: 'utf8' });
}

test('public runtime policy pins only self-authored CC0 metadata and addon', () => {
  const policy = JSON.parse(fs.readFileSync(path.join(root, 'content-src/public-runtime.json'), 'utf8'));
  const modManifest = JSON.parse(fs.readFileSync(path.join(root, 'content-src/baseoq4/mod.json'), 'utf8'));
  const allowlist = JSON.parse(fs.readFileSync(path.join(root, 'content-src/retail-duplicate-allowlist.json'), 'utf8'));
  const source = fs.readFileSync(stage, 'utf8');

  assert.equal(policy.schema, 1);
  assert.equal(policy.policy, 'free-only-v1');
  assert.equal(policy.requiredopenQ4Version, '0.1.010');
  assert.deepEqual(Object.keys(modManifest), [
    'name',
    'version',
    'releaseDate',
    'website',
    'author',
    'requiredopenQ4Version'
  ]);
  assert.ok(Object.values(modManifest).every((value) => typeof value === 'string' && value.length > 0));
  assert.equal(modManifest.requiredopenQ4Version, policy.requiredopenQ4Version);
  assert.deepEqual(allowlist, { schema: 1, entries: [] });
  assert.deepEqual(policy.files.map((entry) => entry.name), [
    'mod.json',
    'pak0.pk4',
    'pak1.pk4',
    'q4web_mp_q4web_arena-89b9c56e5e0a.pk4'
  ]);
  assert.ok(policy.files.every((entry) => entry.license === 'CC0-1.0' && entry.source === 'self-authored'));
  assert.deepEqual(
    policy.files.filter((entry) => entry.kind === 'runtime-pack').map((entry) => entry.name),
    ['pak0.pk4', 'pak1.pk4']
  );
  assert.doesNotMatch(source, /Q4_RUNTIME_SOURCE|coreNames/);
  assert.match(source, /runAudit\(stage, allowlist, retailDirectories\(\)\);[\s\S]*publishDirectory\(stage\)/);
  assert.match(source, /frozen engine was not built against the approved free runtime source/);
});

test('CC0 compatibility declarations preserve replay entityDef registration order', () => {
  const compatibility = fs.readFileSync(
    path.join(root, 'content-src/baseoq4/pak0/def/weapons/00_napalmgun_openq4.def'),
    'utf8',
  );
  const declarations = [...compatibility.matchAll(/^entityDef\s+(\S+)/gm)]
    .map((match) => match[1]);

  assert.match(compatibility, /SPDX-License-Identifier: CC0-1\.0/);
  assert.deepEqual(declarations, [
    'damage_openq4_napalmGlobResidueBurn',
    'impact_napalm_projectile',
  ]);
  assert.match(compatibility, /"spawnclass"\s+"idProjectile"/);
  assert.doesNotMatch(compatibility, /models\/|effects\/|fx_|fuse|gravity|speed/);
});

test('retail audit rejects same-path and renamed byte-identical entries', (t) => {
  const state = fixture(t);
  const publicTree = path.join(state.temp, 'public-tree');
  const retailTree = path.join(state.temp, 'retail-tree');

  fs.mkdirSync(path.join(publicTree, 'gfx'), { recursive: true });
  fs.mkdirSync(path.join(retailTree, 'gfx'), { recursive: true });
  fs.writeFileSync(path.join(publicTree, 'gfx', 'scope.tga'), 'retail bytes');
  fs.writeFileSync(path.join(retailTree, 'gfx', 'scope.tga'), 'retail bytes');
  zipTree(path.join(state.temp, 'public', 'addon.pk4'), publicTree);
  zipTree(path.join(state.temp, 'retail', 'pak001.pk4'), retailTree);

  let result = runAudit(state);
  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /same-path/);

  fs.rmSync(path.join(state.temp, 'public', 'addon.pk4'));
  fs.rmSync(path.join(publicTree, 'gfx'), { recursive: true });
  fs.mkdirSync(path.join(publicTree, 'original'), { recursive: true });
  fs.writeFileSync(path.join(publicTree, 'original', 'different-name.bin'), 'retail bytes');
  zipTree(path.join(state.temp, 'public', 'addon.pk4'), publicTree);

  result = runAudit(state);
  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /same-bytes/);
});

test('free-only staging audits before atomically publishing a minimal runtime', (t) => {
  const state = fixture(t);
  fs.writeFileSync(path.join(state.temp, 'retail-tree', 'unrelated.txt'), 'different private bytes');
  zipTree(path.join(state.temp, 'retail', 'pak001.pk4'), path.join(state.temp, 'retail-tree'));
  const output = path.join(state.temp, 'staged-runtime');
  const report = path.join(state.temp, 'audit.json');
  const buildInfo = path.join(state.temp, 'build-info.txt');
  const nativeOutput = path.join(state.temp, 'native-baseoq4');
  const policy = JSON.parse(fs.readFileSync(path.join(root, 'content-src/public-runtime.json'), 'utf8'));
  fs.writeFileSync(buildInfo, `runtime_source_sha256=${policy.runtimeSourceSha256}\n`);
  const environment = {
    ...process.env,
    Q4_RETAIL_AUDIT_DIRS: path.join(state.temp, 'retail'),
    Q4_NATIVE_CONTENT_OUTPUT: nativeOutput,
    Q4_CONTENT_AUDIT_REPORT: report,
    Q4_ENGINE_BUILD_INFO: buildInfo
  };

  let result = spawnSync(process.execPath, [stage, output], {
    cwd: root,
    encoding: 'utf8',
    env: environment
  });
  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /native archive is missing/);
  assert.equal(fs.existsSync(output), false);

  fs.mkdirSync(nativeOutput);
  zipEntries(path.join(nativeOutput, 'pak0.pk4'), {
    'def/bot_support.def': fs.readFileSync(
      path.join(root, 'content-src/baseoq4/pak0/def/bot_support.def'),
      'utf8'
    ),
    'def/weapons/00_napalmgun_openq4.def': fs.readFileSync(
      path.join(root, 'content-src/baseoq4/pak0/def/weapons/00_napalmgun_openq4.def'),
      'utf8'
    ),
    'native-only.txt': 'generated native runtime fixture'
  });
  zipEntries(path.join(nativeOutput, 'pak1.pk4'), {
    'README.txt': 'generated native pak1 fixture'
  });

  result = spawnSync(process.execPath, [stage, output], {
    cwd: root,
    encoding: 'utf8',
    env: environment
  });
  assert.equal(result.status, 0, result.stderr);
  assert.deepEqual(fs.readdirSync(output).sort(), [
    'manifest.json',
    'mod.json',
    'pak0.pk4',
    'pak1.pk4',
    'q4web_mp_q4web_arena-89b9c56e5e0a.pk4'
  ]);
  const manifest = JSON.parse(fs.readFileSync(path.join(output, 'manifest.json'), 'utf8'));
  assert.equal(manifest.schema, 2);
  assert.equal(manifest.policy, 'free-only-v1');
  assert.match(manifest.build, /^q4web-free-[a-f0-9]{20}$/);
  assert.deepEqual(manifest.files.map((entry) => entry.name), [
    'mod.json',
    'pak0.pk4',
    'pak1.pk4',
    'q4web_mp_q4web_arena-89b9c56e5e0a.pk4'
  ]);
  const sourceModManifest = JSON.parse(fs.readFileSync(path.join(root, 'content-src/baseoq4/mod.json'), 'utf8'));
  const stagedModManifest = JSON.parse(fs.readFileSync(path.join(output, 'mod.json'), 'utf8'));
  assert.deepEqual(stagedModManifest, sourceModManifest);
  assert.equal(stagedModManifest.requiredopenQ4Version, '0.1.010');
  for (const name of ['pak0.pk4', 'pak1.pk4']) {
    assert.equal(fs.statSync(path.join(output, name)).mode & 0o777, 0o644);
  }
  assert.equal(fs.existsSync(path.join(nativeOutput, 'q4web_mp_q4web_arena-89b9c56e5e0a.pk4')), true);
  assert.equal(JSON.parse(fs.readFileSync(report, 'utf8')).duplicates, 0);
});
