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

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

test("Windows installer packages only production assets and blocks unverified signing", async () => {
  const build = await readFile(new URL("windows-installer/build-installer.ps1", root), "utf8");
  assert.match(build, /stage-windows-backend-runtime\.mjs/);
  assert.match(build, /smoke-windows-installed-layout\.mjs/);
  assert.match(build, /smoke-windows-config-migration\.mjs/);
  assert.match(build, /Source: "\$layout\\\*"; DestDir: "\{app\}"/);
  assert.match(build, /PublishSingleFile=true/);
  assert.match(build, /CODE_REMOTE_MANAGER_SIGN_CERT_SHA1/);
  assert.match(build, /LicenseFile=.*TERMS\.txt/);
  assert.match(build, /InfoBeforeFile=.*PRIVACY\.txt/);
  assert.match(build, /NPM-THIRD-PARTY-NOTICES\.txt/);
  assert.match(build, /PWA-THIRD-PARTY-NOTICES\.txt/);
  assert.match(build, /release-candidate\.mjs"\) verify-pwa-notices/);
  assert.match(build, /generate-third-party-notices\.mjs"\) \(Join-Path \$backendRuntime "node_modules"\)/);
  assert.match(build, /DOTNET-THIRD-PARTY-NOTICES\.txt/);
  assert.match(build, /QRCODER-LICENSE\.txt/);
  assert.doesNotMatch(build, /Source: "\$root\\apps\\backend/);
  assert.doesNotMatch(build, /config\.local\.json.*Source/);

  const stageIndex = build.indexOf("stage-windows-backend-runtime.mjs");
  const smokeIndex = build.indexOf("smoke-windows-installed-layout.mjs");
  const configSmokeIndex = build.indexOf("smoke-windows-config-migration.mjs");
  const compileIndex = build.indexOf("& $compiler $iss");
  assert.ok(stageIndex < smokeIndex);
  assert.ok(smokeIndex < compileIndex);
  assert.ok(smokeIndex < configSmokeIndex);
  assert.ok(configSmokeIndex < compileIndex);
});

test("Windows installer registers current-user startup and removes it on uninstall", async () => {
  const build = await readFile(new URL("windows-installer/build-installer.ps1", root), "utf8");
  assert.match(build, /CurrentVersion\\Run/);
  assert.match(build, /uninsdeletevalue/);
  assert.doesNotMatch(build, /ValueType: none; ValueName: "Code Remote Manager Host"/);
  assert.match(build, /PrivilegesRequired=lowest/);
});

test("Windows uninstall retains configuration, backups, authentication, and user data", async () => {
  const build = await readFile(new URL("windows-installer/build-installer.ps1", root), "utf8");
  assert.doesNotMatch(build, /\[UninstallDelete\]/);
  assert.doesNotMatch(build, /config\.local\.json.*(?:delete|remove)/i);
  assert.doesNotMatch(build, /config-backups.*(?:delete|remove)/i);
  assert.doesNotMatch(build, /auth\.json.*(?:delete|remove)/i);
});

test("required npm test includes Setup configuration migration and rollback tests", async () => {
  const manifest = JSON.parse(await readFile(new URL("package.json", root), "utf8"));
  assert.match(
    manifest.scripts.test,
    /dotnet test windows-host\/SetupApp\.Tests\/CodeRemoteManagerSetupApp\.Tests\.csproj/,
  );
});

test("Windows installer exposes Code Remote Manager release metadata and upgrades the existing AppId", async () => {
  const build = await readFile(new URL("windows-installer/build-installer.ps1", root), "utf8");
  assert.match(build, /AppId=\{\{A44FE547-94D5-4A60-80FB-FE7C3BC05461\}/);
  assert.match(build, /AppName=Code Remote Manager/);
  assert.match(build, /AppPublisher=MakeCdev/);
  assert.match(build, /AppPublisherURL=https:\/\/makecdev\.com/);
  assert.match(build, /AppSupportURL=https:\/\/coderemotemanager\.makecdev\.com\/support/);
  assert.match(build, /AppUpdatesURL=https:\/\/coderemotemanager\.makecdev\.com\/download/);
  assert.match(build, /UsePreviousAppDir=yes/);
  assert.match(build, /OutputBaseFilename=CodeRemoteManagerSetup/);
});

test("Windows PowerShell installer source stays ASCII-safe", async () => {
  const build = await readFile(new URL("windows-installer/build-installer.ps1", root), "utf8");
  assert.doesNotMatch(build, /[^\x00-\x7f]/);
});

test("public Windows distribution clearly documents the intentional unsigned release", async () => {
  const [build, notice] = await Promise.all([
    readFile(new URL("windows-installer/build-installer.ps1", root), "utf8"),
    readFile(new URL("distribution/WINDOWS_INSTALL.txt", root), "utf8"),
  ]);

  assert.match(build, /WINDOWS_INSTALL\.txt/);
  assert.match(build, /InfoAfterFile=.*WINDOWS_INSTALL\.txt/);
  assert.match(notice, /unsigned/i);
  assert.match(notice, /unknown publisher/i);
  assert.match(notice, /SHA-256/);
  assert.match(notice, /official download/i);
  assert.match(notice, /does not ask you to disable SmartScreen/i);
  assert.doesNotMatch(notice, /(?:please|must|should) disable (?:SmartScreen|Defender|antivirus)/i);
});

test("Windows release lifecycle uses a removable standard user and verifies retained data", async () => {
  const [manifestText, lifecycle] = await Promise.all([
    readFile(new URL("package.json", root), "utf8"),
    readFile(new URL("tools/verify-windows-clean-user.ps1", root), "utf8"),
  ]);
  const scripts = JSON.parse(manifestText).scripts;

  assert.equal(
    scripts["release:verify-windows-lifecycle"],
    "powershell -NoProfile -ExecutionPolicy Bypass -File tools/verify-windows-clean-user.ps1",
  );
  assert.ok(lifecycle.indexOf("Push-Location $root") < lifecycle.indexOf("npm.cmd run release:verify"));
  assert.match(lifecycle, /finally \{ Pop-Location \}/);
  assert.ok(lifecycle.indexOf("npm.cmd run release:verify") < lifecycle.indexOf("New-LocalUser"));
  assert.match(lifecycle, /New-LocalUser/);
  const localUserDescription = lifecycle.match(/New-LocalUser[\s\S]*?-Description "([^"]+)"/)?.[1];
  assert.ok(localUserDescription && localUserDescription.length <= 48);
  assert.match(lifecycle, /Add-LocalGroupMember/);
  assert.match(lifecycle, /icacls\.exe[\s\S]*\/Q/);
  assert.doesNotMatch(lifecycle, /icacls\.exe[^\r\n]*\/quiet/i);
  assert.match(lifecycle, /Start-Process[\s\S]*-Credential/);
  assert.match(lifecycle, /\/VERYSILENT/);
  assert.match(lifecycle, /unins000\.exe/);
  assert.match(lifecycle, /CurrentVersion\\Run/);
  assert.match(lifecycle, /Start-Process -FilePath "wscript\.exe"/);
  assert.match(lifecycle, /Invoke-CimMethod -InputObject \$_ -MethodName Terminate -ErrorAction SilentlyContinue/);
  assert.match(lifecycle, /retained-marker/);
  assert.match(lifecycle, /data\\security\\device-auth\.json/);
  assert.match(lifecycle, /schemaVersion.*pendingPairing.*devices/);
  assert.match(lifecycle, /\[string\]\$ResultPath/);
  assert.match(lifecycle, /ConvertTo-Json/);
  assert.match(lifecycle, /stage/);
  for (const stage of ["initial-install", "installed-layout", "first-start", "restart", "in-place-update", "post-update-start", "uninstall", "retained-data"]) {
    assert.match(lifecycle, new RegExp(`childStage = "${stage}"`));
  }
  assert.match(lifecycle, /Write-SafeResult \$false \("child-" \+ \[string\]\$result\.stage\)/);
  assert.match(lifecycle, /finally[\s\S]*Remove-LocalUser/);
  assert.doesNotMatch(lifecycle, /Write-(?:Host|Output).*(?:password|credential)/i);
});

test("Windows Setup creates a local five-minute device pairing QR and code", async () => {
  const [form, launcher, cli] = await Promise.all([
    readFile(new URL("windows-host/SetupApp/MainForm.cs", root), "utf8"),
    readFile(new URL("windows-host/SetupApp/DevicePairingLauncher.cs", root), "utf8"),
    readFile(new URL("apps/backend/src/device-pairing-cli.js", root), "utf8")
  ]);

  assert.match(form, /pairingAction\.Click \+= StartPairing/);
  assert.match(form, /RenderQr\(pairing\.QrPayload\)/);
  assert.match(form, /pairingCode\.Text = activePairingCode/);
  assert.match(form, /Expires in/);
  assert.match(form, /HostIsRunningAsync/);
  assert.doesNotMatch(form, /Clipboard\.SetText\(activePairingCode/);
  assert.match(launcher, /RedirectStandardOutput = true/);
  assert.match(launcher, /throw new DevicePairingException\(\)/);
  assert.doesNotMatch(launcher, /StandardError.*(?:result|Message)/s);
  assert.match(cli, /createDeviceAuthStore\(config\.dataDir\)\.createPairing/);
  assert.doesNotMatch(cli, /console\.log/);
});

test("Windows Setup blocks unsafe Tailscale exposure before changing host state", async () => {
  const form = await readFile(new URL("windows-host/SetupApp/MainForm.cs", root), "utf8");
  const methodStart = form.indexOf("private async Task EnsureHostConfiguredAsync()");
  const methodEnd = form.indexOf("private static async Task<bool> HostIsRunningAsync", methodStart);
  const method = form.slice(methodStart, methodEnd);

  assert.ok(methodStart >= 0 && methodEnd > methodStart);
  assert.match(method, /BuildConfigurationCandidateAsync/);
  assert.match(method, /TailscaleExposureGuard/);
  const firstSafetyCheck = method.indexOf("exposure.EnsureSafe(backendPort)");
  const saveConfiguration = method.indexOf("SaveConfigurationAsync(candidate)");
  const configureServe = method.indexOf("RunTailscaleServe()");
  const finalSafetyCheck = method.lastIndexOf("exposure.EnsureSafe(backendPort)");
  assert.ok(firstSafetyCheck >= 0 && firstSafetyCheck < saveConfiguration);
  assert.ok(saveConfiguration < configureServe);
  assert.ok(configureServe < finalSafetyCheck);
});
