import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";

import {
  androidDistributionPolicy,
  elfLoadAlignments,
  validateAndroidBundleMetadata,
  validateAndroidCertificateMetadata,
  validateAndroidManifest,
} from "./release-candidate.mjs";

const root = new URL("../", import.meta.url);

test("Android release packages generated transitive license metadata", async () => {
  const [rootBuild, appBuild] = await Promise.all([
    readFile(new URL("apps/pwa/android/build.gradle", root), "utf8"),
    readFile(new URL("apps/pwa/android/app/build.gradle", root), "utf8"),
  ]);

  assert.match(rootBuild, /com\.github\.jk1:gradle-license-report:3\.1\.4/);
  assert.match(appBuild, /com\.github\.jk1\.dependency-license-report/);
  assert.match(appBuild, /releaseRuntimeClasspath/);
  assert.match(appBuild, /JsonReportRenderer\(['"]android-third-party-licenses\.json['"]\)/);
  assert.match(appBuild, /generated\/licenseReport/);
  assert.match(appBuild, /preBuild[\s\S]*generateLicenseReport/);
  assert.match(appBuild, /generateCapacitorLicenseNotices/);
  for (const packageName of ["android", "app", "barcode-scanner", "browser", "local-notifications", "network", "status-bar"]) {
    assert.match(appBuild, new RegExp(`@capacitor/${packageName.replace("-", "\\-")}`));
  }
});

function elf64WithLoadAlignments(...alignments) {
  const buffer = Buffer.alloc(64 + (56 * alignments.length));
  buffer.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]);
  buffer.writeBigUInt64LE(64n, 32);
  buffer.writeUInt16LE(56, 54);
  buffer.writeUInt16LE(alignments.length, 56);
  alignments.forEach((alignment, index) => {
    const offset = 64 + (56 * index);
    buffer.writeUInt32LE(1, offset);
    buffer.writeBigUInt64LE(BigInt(alignment), offset + 48);
  });
  return buffer;
}

function validMetadata() {
  const nativeLibraries = [];
  for (const name of ["libscanner.so", "libsurface.so"]) {
    for (const abi of ["arm64-v8a", "armeabi-v7a", "x86", "x86_64"]) {
      nativeLibraries.push({
        abi,
        name,
        loadAlignments: [abi === "armeabi-v7a" || abi === "x86" ? 4096 : 16384, 65536],
      });
    }
  }
  return {
    packageName: "com.makecdev.coderemotemanager",
    versionCode: 1,
    versionName: "0.2.0",
    minSdk: 26,
    targetSdk: 36,
    debuggable: false,
    usesCleartextTraffic: false,
    allowBackup: false,
    pageAlignment: "PAGE_ALIGNMENT_16K",
    thirdPartyDependencyCount: 138,
    capacitorLicenseCount: 7,
    nativeLibraries,
  };
}

test("ELF inspection reads every 64-bit LOAD segment alignment", () => {
  assert.deepEqual(elfLoadAlignments(elf64WithLoadAlignments(16384, 65536)), [16384, 65536]);
  assert.throws(() => elfLoadAlignments(Buffer.from("not-elf")), /release_android_invalid_elf/);
});

test("Android bundle metadata requires release identity, paired 64-bit ABIs, and 16 KB alignment", () => {
  assert.deepEqual(validateAndroidBundleMetadata(validMetadata(), "0.2.0"), {
    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: 8,
    thirdPartyDependencyCount: 138,
    capacitorLicenseCount: 7,
    pageSize16Kb: "verified",
  });

  const missingArm64 = validMetadata();
  missingArm64.nativeLibraries = missingArm64.nativeLibraries.filter(({ abi }) => abi !== "arm64-v8a");
  assert.throws(() => validateAndroidBundleMetadata(missingArm64, "0.2.0"), /release_android_64_bit_required/);

  const staleLibrary = validMetadata();
  staleLibrary.nativeLibraries = staleLibrary.nativeLibraries.filter(
    ({ abi, name }) => !(abi === "x86_64" && name === "libsurface.so"),
  );
  assert.throws(() => validateAndroidBundleMetadata(staleLibrary, "0.2.0"), /release_android_native_library_mismatch/);

  const fourKilobyteBundle = validMetadata();
  fourKilobyteBundle.pageAlignment = "PAGE_ALIGNMENT_4K";
  assert.throws(() => validateAndroidBundleMetadata(fourKilobyteBundle, "0.2.0"), /release_android_page_alignment_invalid/);

  const unalignedElf = validMetadata();
  unalignedElf.nativeLibraries[0].loadAlignments = [4096, 16384];
  assert.throws(() => validateAndroidBundleMetadata(unalignedElf, "0.2.0"), /release_android_elf_alignment_invalid/);

  const missingNotices = validMetadata();
  missingNotices.thirdPartyDependencyCount = 0;
  assert.throws(() => validateAndroidBundleMetadata(missingNotices, "0.2.0"), /release_android_license_notices_missing/);
});

test("Android bundle metadata rejects stale manifest SDK and version values", () => {
  for (const [field, value] of [
    ["packageName", "com.example.debug"],
    ["versionCode", 0],
    ["versionName", "0.1.0"],
    ["minSdk", 25],
    ["targetSdk", 35],
  ]) {
    const metadata = validMetadata();
    metadata[field] = value;
    assert.throws(() => validateAndroidBundleMetadata(metadata, "0.2.0"), /release_android_manifest_mismatch/);
  }
});

test("Android release manifest rejects unsafe application attributes", () => {
  const safeApplication = 'android:usesCleartextTraffic="false" android:allowBackup="false"';
  for (const attribute of [
    'android:debuggable="true"',
    'android:usesCleartextTraffic="true"',
    'android:allowBackup="true"',
  ]) {
    const application = attribute.startsWith("android:debuggable")
      ? `${safeApplication} ${attribute}`
      : `${safeApplication.replace(/android:(?:usesCleartextTraffic|allowBackup)="false"/g, (value) =>
        value.startsWith(attribute.split("=")[0]) ? attribute : value)} `;
    const manifest = `<manifest package="com.makecdev.coderemotemanager"><application ${application}/></manifest>`;
    assert.throws(() => validateAndroidManifest(manifest), /release_android_manifest_hardening_invalid/);
  }
});

test("Android release manifest accepts omitted or false debuggable with hardened defaults", () => {
  for (const debuggable of ["", ' android:debuggable="false"']) {
    const manifest = `<manifest package="com.makecdev.coderemotemanager"><application` +
      `${debuggable} android:usesCleartextTraffic="false" android:allowBackup="false"/></manifest>`;
    assert.deepEqual(validateAndroidManifest(manifest), {
      packageName: "com.makecdev.coderemotemanager",
      debuggable: false,
      usesCleartextTraffic: false,
      allowBackup: false,
    });
  }
  const wrongPackage = '<manifest package="com.example.debug"><application ' +
    'android:usesCleartextTraffic="false" android:allowBackup="false"/></manifest>';
  assert.throws(() => validateAndroidManifest(wrongPackage), /release_android_manifest_mismatch/);
});

test("Android candidate and submission signing states are explicit and debug certificates are rejected", () => {
  assert.deepEqual(androidDistributionPolicy({ signing: "unsigned" }), {
    channel: "pre-signing-candidate",
    signature: "unsigned",
    verification: "bundletool-manifest-and-sha256",
  });
  assert.deepEqual(androidDistributionPolicy({
    signing: "signed",
    certificateSha256: "a".repeat(64),
  }), {
    channel: "play-upload-candidate",
    signature: "signed",
    verification: "jarsigner-certificate-and-sha256-manifest",
    certificateSha256: "a".repeat(64),
  });

  for (const metadata of [
    { subject: "CN=Android Debug, O=Android, C=US" },
    { subject: "CN=Android Debug Certificate, O=Android, C=US" },
    { subject: "CN=Local Debug Signing, O=MakeCdev, C=KR" },
    { subject: "CN=Code Remote Manager Release, O=MakeCdev, C=KR", keyAlias: "androiddebugkey" },
  ]) {
    assert.throws(() => validateAndroidCertificateMetadata({
      ...metadata,
      certificateSha256: "a".repeat(64),
    }, "a".repeat(64)), /release_android_debug_certificate/);
  }
  assert.equal(validateAndroidCertificateMetadata({
    subject: "CN=Code Remote Manager Release, O=MakeCdev, C=KR",
    keyAlias: "code-remote-manager-release",
    certificateSha256: "a".repeat(64),
  }, "a".repeat(64)), "a".repeat(64));
});
