import test from "node:test";
import assert from "node:assert/strict";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

const root = new URL("../", import.meta.url);
const packagePaths = [
  "package.json",
  "apps/backend/package.json",
  "apps/pwa/package.json",
  "packages/shared/package.json",
  "tools/fake-codex/package.json",
];

async function read(relativePath) {
  return readFile(new URL(relativePath, root), "utf8");
}

test("source package metadata uses the approved release version", async () => {
  const releaseVersion = JSON.parse(await read("package.json")).version;
  assert.equal(releaseVersion, "1.0.0");

  for (const packagePath of packagePaths) {
    const manifest = JSON.parse(await read(packagePath));
    assert.equal(manifest.version, releaseVersion, packagePath);
  }

  const lock = JSON.parse(await read("package-lock.json"));
  for (const packagePath of packagePaths) {
    const lockPath = packagePath === "package.json" ? "" : packagePath.slice(0, -"/package.json".length);
    assert.equal(lock.packages[lockPath].version, releaseVersion, `package-lock.json:${lockPath || "<root>"}`);
  }
});

test("Windows release metadata uses the approved release version", async () => {
  const [manifest, installer, project] = await Promise.all([
    read("package.json"),
    read("windows-installer/build-installer.ps1"),
    read("windows-host/SetupApp/CodeRemoteManagerSetupApp.csproj"),
  ]);
  const releaseVersion = JSON.parse(manifest).version;

  assert.match(installer, /^\$version = \(Get-Content -LiteralPath \(Join-Path \$root "package\.json"\) -Raw \| ConvertFrom-Json\)\.version$/m);
  assert.equal(project.match(/<Version>([^<]+)<\/Version>/)?.[1], releaseVersion);
  assert.equal(project.match(/<FileVersion>([^<]+)<\/FileVersion>/)?.[1], `${releaseVersion}.0`);
  assert.match(installer, /^AppVersion=\$version$/m);
  assert.match(installer, /^VersionInfoVersion=\$version$/m);
});

test("Android release metadata uses the approved release version", async () => {
  const [manifest, gradle] = await Promise.all([
    read("package.json"),
    read("apps/pwa/android/app/build.gradle"),
  ]);
  const releaseVersion = JSON.parse(manifest).version;
  const versionCode = Number(gradle.match(/^\s*versionCode\s+(\d+)$/m)?.[1]);

  assert.match(gradle, /^def releaseVersion = new groovy\.json\.JsonSlurper\(\)\.parse\(rootProject\.file\("\.\.\/\.\.\/\.\.\/package\.json"\)\)\.version$/m);
  assert.match(gradle, /^\s*versionName releaseVersion$/m);
  assert.ok(Number.isInteger(versionCode) && versionCode > 1, "Android versionCode must exceed the previous upload");
});

test("release commands produce one verified candidate set and isolate Android signing secrets", async () => {
  const [manifest, installer, androidBuild, releaseTool] = await Promise.all([
    read("package.json"),
    read("windows-installer/build-installer.ps1"),
    read("apps/pwa/android/build.gradle"),
    read("tools/release-candidate.mjs"),
  ]);
  const scripts = JSON.parse(manifest).scripts;

  assert.equal(scripts["release:preflight"], "node tools/release-pwa-policy.mjs");
  assert.equal(scripts["release:prepare"], "node tools/release-candidate.mjs prepare");
  assert.equal(scripts["release:verify"], "node tools/release-candidate.mjs verify");
  assert.equal(scripts["release:verify-android-device"], "node tools/release-candidate.mjs verify-android-device");
  assert.match(installer, /\[switch\]\$UseExistingPwa/);
  assert.match(installer, /\[switch\]\$ReleaseCandidate/);
  assert.match(installer, /release-candidate\.mjs"\) verify-pwa-copy/);
  assert.ok(installer.indexOf("verify-pwa-copy") < installer.indexOf("& $compiler $iss"));
  assert.match(installer, /CODE_REMOTE_MANAGER_SIGN_CERT_SHA1[\s\S]*Signing requires the verified release workflow/);
  assert.doesNotMatch(installer, /signtool sign/);
  assert.match(androidBuild, /com\.android\.tools\.build:bundletool:1\.18\.1/);
  assert.match(androidBuild, /task releaseBundletool\(type: JavaExec\)/);
  assert.match(androidBuild, /"device-apks"/);
  assert.match(androidBuild, /CODE_REMOTE_MANAGER_ANDROID_AAPT2/);
  assert.match(androidBuild, /--aapt2=\$\{aapt2\}/);
  assert.match(androidBuild, /--ks-pass=file:\$\{storePasswordFile\}/);
  assert.doesNotMatch(androidBuild, /--ks-pass=(?:pass|env):/);
  assert.match(releaseTool, /CODE_REMOTE_MANAGER_ANDROID_KEYSTORE/);
  assert.match(releaseTool, /CODE_REMOTE_MANAGER_ANDROID_CERT_SHA256/);
  assert.match(releaseTool, /-storepass:env/);
  assert.match(releaseTool, /CODE_REMOTE_MANAGER_ANDROID_AAPT2:\s*buildTools\.aapt2/);
  assert.match(releaseTool, /import \{ runReleasePreflight \} from "\.\/release-pwa-policy\.mjs"/);
  assert.match(releaseTool, /async function prepareRelease\(\)[\s\S]*await runReleasePreflight\(\)/);
  assert.ok(
    releaseTool.indexOf("await runReleasePreflight()") < releaseTool.indexOf("await mkdir(candidateDirectory"),
    "release preflight must finish before the candidate directory is created"
  );
  assert.match(releaseTool, /async function prepareRelease\(\)[\s\S]*await assertPwaNoticesCurrent\(\)/);
  assert.match(releaseTool, /async function verifyPreparedRelease\(\)[\s\S]*await assertPwaNoticesCurrent\(\)/);
  assert.match(releaseTool, /action === "verify-pwa-notices"[\s\S]*assertPwaNoticesCurrent/);
  assert.match(releaseTool, /async function signPreparedAndroidRelease\(\)/);
  assert.match(releaseTool, /action === "sign-android-candidate"[\s\S]*signPreparedAndroidRelease\(\)/);
  assert.doesNotMatch(releaseTool, /"-storepass",\s*process\.env/);
});

test("release verification rejects a stale committed PWA dependency notice", async (t) => {
  const { assertPwaNoticesCurrent } = await import("./release-candidate.mjs");
  const directory = await mkdtemp(join(tmpdir(), "crm-release-notices-test-"));
  t.after(() => rm(directory, { recursive: true, force: true }));
  const modules = join(directory, "node_modules");
  const packageDirectory = join(modules, "alpha");
  const manifestPath = join(directory, "pwa-package.json");
  const noticePath = join(directory, "PWA-THIRD-PARTY-NOTICES.txt");
  await mkdir(packageDirectory, { recursive: true });
  await writeFile(manifestPath, JSON.stringify({ dependencies: { alpha: "1.0.0" } }));
  await writeFile(join(packageDirectory, "package.json"), JSON.stringify({
    name: "alpha",
    version: "1.0.0",
    license: "MIT",
  }));
  await writeFile(join(packageDirectory, "LICENSE"), "alpha license");

  const generated = `THIRD-PARTY NPM NOTICES\n\nalpha 1.0.0\nLicense: MIT\n\nalpha license\n`;
  await writeFile(noticePath, generated);
  await assertPwaNoticesCurrent({ modules, manifestPath, noticePath });
  await writeFile(noticePath, "stale");
  await assert.rejects(
    assertPwaNoticesCurrent({ modules, manifestPath, noticePath }),
    /release_pwa_notices_stale/,
  );
});

test("release manifest detects stale PWA copies and changed artifacts without storing local paths", async (t) => {
  const { createReleaseManifest, hashTree, verifyPwaCopy, verifyReleaseManifest } = await import("./release-candidate.mjs");
  const directory = await mkdtemp(join(tmpdir(), "crm-release-test-"));
  t.after(() => rm(directory, { recursive: true, force: true }));
  const sourcePwa = join(directory, "pwa");
  const copiedPwa = join(directory, "copy");
  const candidateDir = join(directory, "candidate");
  await mkdir(join(sourcePwa, "assets"), { recursive: true });
  await mkdir(join(copiedPwa, "assets"), { recursive: true });
  await mkdir(candidateDir, { recursive: true });
  await writeFile(join(sourcePwa, "index.html"), "current");
  await writeFile(join(sourcePwa, "assets", "app.js"), "current-js");
  await writeFile(join(copiedPwa, "index.html"), "current");
  await writeFile(join(copiedPwa, "assets", "app.js"), "current-js");

  const pwa = await hashTree(sourcePwa);
  await verifyPwaCopy(sourcePwa, copiedPwa);
  await writeFile(join(copiedPwa, "index.html"), "stale");
  await assert.rejects(verifyPwaCopy(sourcePwa, copiedPwa), /release_pwa_mismatch/);

  await writeFile(join(candidateDir, "source.zip"), "source");
  await writeFile(join(candidateDir, "setup.exe"), "windows");
  await writeFile(join(candidateDir, "app.aab"), "android");
  const source = { commit: "a".repeat(40), tree: "b".repeat(40) };
  const android = {
    packageName: "com.makecdev.coderemotemanager",
    versionCode: 1,
    versionName: "0.2.0",
    minSdk: 26,
    targetSdk: 36,
    debuggable: false,
    usesCleartextTraffic: false,
    allowBackup: false,
    abis: ["arm64-v8a", "armeabi-v7a", "x86", "x86_64"],
    nativeLibraryCount: 4,
    thirdPartyDependencyCount: 138,
    capacitorLicenseCount: 7,
    pageSize16Kb: "verified",
  };
  const releaseManifest = await createReleaseManifest({
    candidateDir,
    version: "0.2.0",
    source,
    pwa,
    toolchain: { node: "22.17.0", npm: "11.12.1" },
    android: { ...android, certificateSha256: "c".repeat(64) },
    artifacts: {
      source: { file: "source.zip", signing: "not-applicable" },
      windows: { file: "setup.exe", signing: "unsigned" },
      android: { file: "app.aab", signing: "signed" },
    },
  });

  assert.equal(releaseManifest.schemaVersion, 2);
  assert.deepEqual(releaseManifest.distribution, {
    windows: {
      channel: "public-unsigned",
      signature: "unsigned",
      verification: "sha256-manifest",
    },
    android: {
      channel: "play-upload-candidate",
      signature: "signed",
      verification: "jarsigner-certificate-and-sha256-manifest",
      certificateSha256: "c".repeat(64),
    },
  });
  assert.deepEqual(releaseManifest.android, android);
  assert.equal(JSON.stringify(releaseManifest).includes(directory), false);
  await verifyReleaseManifest({ candidateDir, manifest: releaseManifest, source });
  const unsignedManifest = await createReleaseManifest({
    candidateDir,
    version: "0.2.0",
    source,
    pwa,
    toolchain: { node: "22.17.0", npm: "11.12.1" },
    android,
    artifacts: {
      source: { file: "source.zip", signing: "not-applicable" },
      windows: { file: "setup.exe", signing: "unsigned" },
      android: { file: "app.aab", signing: "unsigned" },
    },
  });
  assert.deepEqual(unsignedManifest.distribution.android, {
    channel: "pre-signing-candidate",
    signature: "unsigned",
    verification: "bundletool-manifest-and-sha256",
  });
  assert.equal(unsignedManifest.schemaVersion, 2);
  assert.equal(unsignedManifest.artifacts.android.signing, "unsigned");
  await verifyReleaseManifest({ candidateDir, manifest: unsignedManifest, source });
  const changedPolicy = structuredClone(releaseManifest);
  changedPolicy.distribution.windows.channel = "signed";
  await assert.rejects(
    verifyReleaseManifest({ candidateDir, manifest: changedPolicy, source }),
    /release_manifest_invalid_windows_distribution/,
  );
  await writeFile(join(candidateDir, "setup.exe"), "changed");
  await assert.rejects(
    verifyReleaseManifest({ candidateDir, manifest: releaseManifest, source }),
    /release_artifact_hash_mismatch/,
  );
});
