import { describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { readProviderAppsConfig } from "../../src/components/provider-apps/config";
import { readProviderRelease, releaseResponse } from "../../src/components/provider-apps/release";
import { providerAppsHelp } from "./provider-apps";
import { runProviderAppsLinuxCommand } from "./provider-apps-linux";

const repositoryRoot = resolve(import.meta.dir, "../..");

describe("Provider Apps L1 release", () => {
  const config = readProviderAppsConfig(repositoryRoot);
  const release = readProviderRelease(repositoryRoot, config);

  test("owning YAML declares a fixed native HTTPS origin", () => {
    expect(config.target.id).toBe("NC01");
    expect(config.runtime.port).toBe(18121);
    expect(config.publicExposure.publicBaseUrl).toBe("https://apps.hwpod.com");
    expect(config.install.linux.targets["D601-VM-HOST"]?.server).toBe(
      config.install.defaults.server,
    );
    expect(config.install.linux.targets["D601-VM-HOST"]?.network).toEqual({
      bypassKubernetesServiceNat: true,
      systemdUnit: "unideskcprovider-network.service",
    });
    expect(config.install.linux.targets["D601-VM-HOST"]?.user).toBe("root");
    expect(config.install.linux.targets["D601-VM-HOST"]?.home).toBe("/root");
  });

  test("serves release files and rejects unknown or mutating requests", async () => {
    expect((await releaseResponse("GET", config.release.routes.health, config, release).json()).ok).toBe(true);
    expect(releaseResponse("GET", config.release.routes.manifest, config, release).status).toBe(200);
    expect(releaseResponse("HEAD", config.release.routes.provider, config, release).status).toBe(200);
    expect(releaseResponse("GET", "/private", config, release).status).toBe(404);
    expect(releaseResponse("POST", config.release.routes.provider, config, release).status).toBe(405);
  });

  test("serves human and agent installer surfaces from one L1 service", async () => {
    const index = releaseResponse(
      "GET",
      config.install.routes.index,
      config,
      release,
      "https://apps.hwpod.com",
    );
    expect(index.status).toBe(200);
    expect(index.headers.get("content-security-policy")).toContain("default-src 'self'");
    const indexHtml = await index.text();
    expect(indexHtml).toContain("32 条 TCP warm channel");
    expect(indexHtml).toContain('<option value="auto" selected>自动选择（推荐）</option>');
    expect(indexHtml).toContain('<link rel="icon" href="/assets/provider-icon.svg"');
    const appScript = await releaseResponse(
      "GET",
      config.install.routes.script,
      config,
      release,
    ).text();
    const referencedIds = [...appScript.matchAll(/querySelector\("#([^"]+)"\)/gu)]
      .map((match) => match[1]);
    expect(referencedIds.length).toBeGreaterThan(0);
    for (const id of referencedIds) {
      expect(indexHtml).toContain(`id="${id}"`);
    }
    const contract = await releaseResponse(
      "GET",
      config.install.routes.contract,
      config,
      release,
      "https://apps.hwpod.com",
    ).json() as Record<string, any>;
    expect(contract.schemaVersion).toBe(4);
    expect(contract.platformsSupported).toEqual(["windows", "linux"]);
    expect(contract.humanUrl).toBe("https://apps.hwpod.com/");
    expect(contract.sensitiveInputs.acceptedByWeb).toBe(false);
    expect(contract.installer.acceptsArguments).toBe(false);
    expect(contract.config.mustExistBeforeInstall).toBe(true);
    expect(contract.platforms.windows.oneLineTemplate).toBe(
      'certutil -urlcache -f https://apps.hwpod.com/win.cmd "%TEMP%\\u.cmd"&&call "%TEMP%\\u.cmd"',
    );
    expect(contract.platforms.windows.launchers.cmd.command).toContain("https://apps.hwpod.com/win.cmd");
    expect(contract.platforms.windows.launchers.cmd.support).toContain("explicit runtime modes never fall back");
    expect(contract.platforms.linux.oneLineTemplate).toBe("curl -fsSL https://apps.hwpod.com/linux.sh | bash");
    expect(contract.platforms.linux.configPath).toBe("~/.unidesk/provider.yaml");
    expect(contract.platforms.linux.launchers.bash.support).toContain("no Docker or Node");
    expect(contract.config.pythonRuntime.behavior.auto).toContain("system Python, then a private Python");
    expect(contract.config.pythonRuntime.behavior.system).toContain("fail");
    expect(contract.config.pythonRuntime.behavior.private).toContain("without scanning system Python");
    expect(contract.endpoints.runtimes.windows7.version).toBe("3.8.10");
    expect(contract.endpoints.runtimes.windowsModern.version).toBe("3.11.9");
    const fallbackContract = await releaseResponse(
      "GET",
      config.install.routes.contract,
      config,
      release,
      "http://127.0.0.1:18121",
    ).json() as Record<string, any>;
    expect(fallbackContract.humanUrl).toBe("https://apps.hwpod.com/");
  });

  test("PowerShell installer has release URLs resolved and accepts no arguments", async () => {
    const installer = await releaseResponse(
      "GET",
      config.install.routes.installer,
      config,
      release,
    ).text();
    expect(installer).toContain("https://apps.hwpod.com/v1/unideskcprovider/update.json");
    expect(installer).toContain("https://apps.hwpod.com/v1/unideskcprovider/unideskcprovider.py");
    expect(installer).toContain("$ErrorActionPreference = \"Continue\"");
    expect(installer).toContain("sys.modules[spec.name] = module");
    expect(installer).toContain("\"config-probe-$PID.py\"");
    expect(installer).not.toContain("\"-c\",\n        $configProbeCode");
    expect(installer).toContain("\"$providerPath.install.bak\"");
    expect(installer).not.toContain("$providerPath, $null");
    expect(installer).toContain("param()");
    expect(installer).not.toContain("__MANIFEST_URL__");
    expect(installer).not.toContain("__PROVIDER_URL__");
    expect(installer).not.toContain("[Parameter(Mandatory");
    expect(installer).not.toContain("nodeRequired");
    expect(releaseResponse("HEAD", config.install.routes.style, config, release).status).toBe(200);
    expect(releaseResponse("HEAD", config.install.routes.script, config, release).status).toBe(200);
    expect(releaseResponse("HEAD", config.install.routes.favicon, config, release).status).toBe(200);
    expect(releaseResponse("HEAD", config.install.routes.panelImage, config, release).status).toBe(200);
    const startCmd = await releaseResponse(
      "GET",
      config.install.routes.startCmd,
      config,
      release,
    ).text();
    expect(startCmd).toContain("[reuse] private Python");
    expect(startCmd).toContain("system_python_unavailable");
    expect(startCmd.indexOf("Find-Python $runtime.minimumVersion")).toBeLessThan(
      startCmd.indexOf('Test-Python ([System.IO.Path]::Combine($runtimeRoot'),
    );
    expect(startCmd).toContain("speed={3:N1} KiB/s elapsed={4}s eta={5}s");
    expect(startCmd).toContain("registry.npmmirror.com");
    expect(startCmd).toContain("www.python.org");
    expect(startCmd).not.toMatch(/__[A-Z_]+__/u);
    const bootstrap = await releaseResponse(
      "GET",
      config.install.routes.bootstrap,
      config,
      release,
    ).text();
    expect(bootstrap).toContain("https://apps.hwpod.com/v1/unideskcprovider/update.json");
    expect(bootstrap).toContain("schedule_provider_handoff");
    expect(bootstrap).toContain("([wmiclass]'Win32_Process').Create");
    expect(bootstrap).not.toContain("stop_existing_provider");
    expect(bootstrap).not.toMatch(/__[A-Z_]+__/u);
  });

  test("Linux installer is zero-argument, YAML-first, and systemd native", async () => {
    const installer = await releaseResponse(
      "GET",
      config.install.routes.linuxInstaller,
      config,
      release,
    ).text();
    expect(installer).toContain('config_path="${config_root}/provider.yaml"');
    expect(installer).toContain("pythonRuntime");
    expect(installer).toContain("systemctl enable");
    expect(installer).toContain("Restart=always");
    expect(installer).toContain('"$config_root/logs"');
    expect(installer).toContain("curl --fail --location --show-error --progress-bar");
    expect(installer).not.toContain("docker");
    expect(installer).not.toContain("node");
    expect(installer).not.toMatch(/__[A-Z_]+__/u);
    expect(releaseResponse("HEAD", config.install.routes.linuxInstaller, config, release).status).toBe(200);
  });

  test("CLI help keeps one YAML-owned native lifecycle", () => {
    const help = providerAppsHelp();
    expect(help.config).toBe("config/platform-infra/apps.yaml");
    expect(help.usage).toContain("bun scripts/cli.ts provider-apps server status");
    expect(help.usage).toContain("bun scripts/cli.ts provider-apps linux help");
  });

  test("Linux apply jobs expose bounded status and logs without touching a runtime", async () => {
    const stateDir = mkdtempSync(join(tmpdir(), "provider-apps-linux-job-test-"));
    const jobId = "abcdef123456";
    const jobRoot = join(stateDir, "linux-jobs", jobId);
    const testConfig = { ...config, runtime: { ...config.runtime, stateDir, maxLogTailBytes: 8 } };
    try {
      mkdirSync(jobRoot, { recursive: true });
      writeFileSync(join(jobRoot, "status.json"), JSON.stringify({
        jobId,
        operation: "provider-apps.linux.apply",
        target: "D601-VM-HOST",
        status: "succeeded",
      }));
      writeFileSync(join(jobRoot, "stdout.log"), "prefix-complete\n");
      const status = await runProviderAppsLinuxCommand(testConfig, ["apply-status", "--target", "D601-VM-HOST", "--id", jobId]);
      const logs = await runProviderAppsLinuxCommand(testConfig, ["apply-logs", "--target", "D601-VM-HOST", "--id", jobId]);
      expect(status).toMatchObject({ ok: true, operation: "provider-apps.linux.apply-status", jobId, status: "succeeded", running: false });
      expect(logs).toMatchObject({ ok: true, operation: "provider-apps.linux.apply-logs", jobId, stdoutTail: "omplete\n", stderrTail: "" });

      writeFileSync(join(jobRoot, "status.json"), JSON.stringify({
        jobId,
        operation: "provider-apps.linux.apply",
        target: "D601-VM-HOST",
        status: "queued",
      }));
      writeFileSync(join(jobRoot, "runner.pid"), "0\n");
      const stopped = await runProviderAppsLinuxCommand(testConfig, ["apply-status", "--target", "D601-VM-HOST", "--id", jobId]);
      expect(stopped).toMatchObject({
        ok: false,
        status: "failed",
        recordedStatus: "queued",
        running: false,
        error: expect.stringContaining("exited before recording a terminal status"),
      });
    } finally {
      rmSync(stateDir, { recursive: true, force: true });
    }
  });
});
