import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { promises as fs } from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { runCli } from "../src/cli.mjs";
import { isSafeRelativePath } from "../src/project-doctor.mjs";

function invoke(args, cwd = process.cwd()) {
  let stdout = "";
  let stderr = "";
  return runCli(args, { cwd, stdout: (value) => { stdout += value; }, stderr: (value) => { stderr += value; } }).then((code) => ({ code, stdout, stderr }));
}

function launchDossierFixture({ manifestId = "last-mile-delivery-web-durable-operation", manifestSha256 = "a".repeat(64) } = {}) {
  const authorityIds = ["source-contract", "application-configuration", "provider-conformance", "security-privacy", "operations", "production-release", "deployment"];
  const payload = {
    schemaVersion: "mappls.application-launch-dossier.v1",
    dossierId: "last-mile-delivery-web-durable-operation.app_delivery",
    application: { id: "app_delivery", environment: "staging" },
    decision: { status: "ready-for-independent-validation", productionReady: false },
    evidence: { integrationManifest: { manifestId, sha256: manifestSha256, sourceFingerprints: ["b".repeat(64)] } },
    authorities: authorityIds.map((id, index) => ({ id, required: true, status: index < 2 ? "satisfied" : "unproven" })),
    safety: { providerCredentialsAccepted: false, providerPayloadsAccepted: false, providerNetworkCalls: 0, writesExposed: false, secretsIncluded: false, privateDestinationsIncluded: false },
  };
  return { ...payload, integrity: { algorithm: "sha256", payloadSha256: createHash("sha256").update(JSON.stringify(payload), "utf8").digest("hex"), authenticity: "self-generated-not-signed" } };
}

test("lists exactly one starter for every published platform", async () => {
  const result = await invoke(["platforms", "--json"]);
  assert.equal(result.code, 0);
  const body = JSON.parse(result.stdout);
  assert.equal(body.count, 13);
  assert.equal(new Set(body.platforms.map((item) => item.platform)).size, 13);
  assert.ok(body.platforms.every((item) => item.readiness && item.language));
});

test("search ranks the canonical catalog and supports bounded JSON output", async () => {
  const result = await invoke(["search", "fleet geofence", "--limit", "3", "--json"]);
  assert.equal(result.code, 0);
  const body = JSON.parse(result.stdout);
  assert.ok(body.count > 0 && body.count <= 3);
  assert.ok(body.results.some((item) => /geofence/i.test(`${item.title} ${item.keywords}`)));
  assert.ok(body.results.every((item) => item.href.startsWith("/")));
});

test("creates and diagnoses a complete fixture-first REST project", async () => {
  const parent = await fs.mkdtemp(path.join(os.tmpdir(), "mappls-cli-create-"));
  try {
    const created = await invoke(["starter", "create", "REST", "--dir", "service", "--json"], parent);
    assert.equal(created.code, 0, created.stderr);
    const body = JSON.parse(created.stdout);
    assert.equal(body.liveProviderCallsEnabled, false);
    assert.ok(body.files >= 8);
    const doctor = await invoke(["doctor", "service", "--json"], parent);
    assert.equal(doctor.code, 0, doctor.stderr);
    const report = JSON.parse(doctor.stdout);
    assert.equal(report.status, "pass");
    assert.equal(report.starter.platform, "REST");
  } finally { await fs.rm(parent, { recursive: true, force: true }); }
});

test("selection-required starters retain a visible doctor warning", async () => {
  const parent = await fs.mkdtemp(path.join(os.tmpdir(), "mappls-cli-selection-"));
  try {
    assert.equal((await invoke(["starter", "create", "Xamarin", "--dir", "app"], parent)).code, 0);
    const doctor = await invoke(["doctor", "app", "--json"], parent);
    const report = JSON.parse(doctor.stdout);
    assert.equal(doctor.code, 0);
    assert.equal(report.status, "warn");
    assert.match(report.warnings.join(" "), /selection|required|legacy|entitlement/i);
  } finally { await fs.rm(parent, { recursive: true, force: true }); }
});

test("creates and validates a deterministic integration manifest offline", async () => {
  const parent = await fs.mkdtemp(path.join(os.tmpdir(), "mappls-cli-manifest-"));
  try {
    const created = await invoke(["manifest", "create", "--use-case", "last-mile-delivery", "--platform", "Android", "--mode", "durable-operation", "--out", "delivery.mappls.json", "--json"], parent);
    assert.equal(created.code, 0, created.stderr);
    const creation = JSON.parse(created.stdout);
    assert.equal(creation.manifestId, "last-mile-delivery-android-durable-operation");
    assert.equal(creation.providerNetworkCalls, 0);
    assert.ok(creation.contracts > 0 && creation.sourceFingerprints > 0);
    const validated = await invoke(["manifest", "validate", "delivery.mappls.json", "--json"], parent);
    assert.equal(validated.code, 0, validated.stderr);
    const report = JSON.parse(validated.stdout);
    assert.equal(report.status, "pass");
    assert.equal(report.applicationBound, false);
  } finally { await fs.rm(parent, { recursive: true, force: true }); }
});

test("manifest validation fails closed on drift and create never overwrites", async () => {
  const parent = await fs.mkdtemp(path.join(os.tmpdir(), "mappls-cli-manifest-drift-"));
  try {
    const args = ["manifest", "create", "--use-case", "travel-discovery", "--platform", "Web", "--mode", "composed-experience", "--out", "mappls.integration.json"];
    assert.equal((await invoke(args, parent)).code, 0);
    assert.equal((await invoke(args, parent)).code, 1);
    const file = path.join(parent, "mappls.integration.json");
    const manifest = JSON.parse(await fs.readFile(file, "utf8"));
    manifest.selection.outcome = "tampered";
    await fs.writeFile(file, `${JSON.stringify(manifest, null, 2)}\n`);
    const validation = await invoke(["manifest", "validate", "mappls.integration.json", "--json"], parent);
    assert.equal(validation.code, 1);
    assert.match(JSON.parse(validation.stdout).failures.join(" "), /canonical fields differ/i);
  } finally { await fs.rm(parent, { recursive: true, force: true }); }
});

test("validates a self-verifying launch dossier without granting production authority", async () => {
  const parent = await fs.mkdtemp(path.join(os.tmpdir(), "mappls-cli-dossier-"));
  try {
    await fs.writeFile(path.join(parent, "launch.mappls.json"), `${JSON.stringify(launchDossierFixture(), null, 2)}\n`);
    const validated = await invoke(["dossier", "validate", "launch.mappls.json", "--json"], parent);
    assert.equal(validated.code, 0, validated.stderr);
    const report = JSON.parse(validated.stdout);
    assert.equal(report.status, "pass");
    assert.equal(report.dossier.productionReady, false);
    assert.equal(report.dossier.requiredAuthorities, 7);
  } finally { await fs.rm(parent, { recursive: true, force: true }); }
});

test("launch dossier validation rejects tampering and self-approved authority", async () => {
  const parent = await fs.mkdtemp(path.join(os.tmpdir(), "mappls-cli-dossier-drift-"));
  try {
    const dossier = launchDossierFixture();
    dossier.decision.productionReady = true;
    dossier.authorities.find((item) => item.id === "production-release").status = "satisfied";
    await fs.writeFile(path.join(parent, "launch.mappls.json"), `${JSON.stringify(dossier, null, 2)}\n`);
    const validated = await invoke(["dossier", "validate", "launch.mappls.json", "--json"], parent);
    assert.equal(validated.code, 1);
    const failures = JSON.parse(validated.stdout).failures.join(" ");
    assert.match(failures, /integrity does not match/i);
    assert.match(failures, /must never claim production readiness/i);
    assert.match(failures, /cannot satisfy independent/i);
  } finally { await fs.rm(parent, { recursive: true, force: true }); }
});

test("launch dossier validation cross-checks the canonical application manifest", async () => {
  const parent = await fs.mkdtemp(path.join(os.tmpdir(), "mappls-cli-dossier-manifest-"));
  try {
    const created = await invoke(["manifest", "create", "--use-case", "last-mile-delivery", "--platform", "Web", "--mode", "durable-operation", "--out", "integration.mappls.json"], parent);
    assert.equal(created.code, 0, created.stderr);
    const manifest = JSON.parse(await fs.readFile(path.join(parent, "integration.mappls.json"), "utf8"));
    const dossier = launchDossierFixture({ manifestId: manifest.manifestId, manifestSha256: createHash("sha256").update(JSON.stringify(manifest), "utf8").digest("hex") });
    await fs.writeFile(path.join(parent, "launch.mappls.json"), `${JSON.stringify(dossier, null, 2)}\n`);
    const validated = await invoke(["dossier", "validate", "launch.mappls.json", "--manifest", "integration.mappls.json", "--json"], parent);
    assert.equal(validated.code, 0, validated.stderr);
    assert.equal(JSON.parse(validated.stdout).dossier.manifestCrossChecked, true);
  } finally { await fs.rm(parent, { recursive: true, force: true }); }
});

test("creation refuses unknown starters and non-empty targets", async () => {
  const parent = await fs.mkdtemp(path.join(os.tmpdir(), "mappls-cli-refuse-"));
  try {
    await fs.writeFile(path.join(parent, "owned.txt"), "preserve me\n");
    const unknown = await invoke(["starter", "create", "unknown"], parent);
    assert.equal(unknown.code, 1);
    const occupied = await invoke(["starter", "create", "REST", "--dir", "."], parent);
    assert.equal(occupied.code, 1);
    assert.equal(await fs.readFile(path.join(parent, "owned.txt"), "utf8"), "preserve me\n");
  } finally { await fs.rm(parent, { recursive: true, force: true }); }
});

test("doctor fails closed on missing files and credential literals", async () => {
  const parent = await fs.mkdtemp(path.join(os.tmpdir(), "mappls-cli-doctor-"));
  try {
    assert.equal((await invoke(["starter", "create", "REST", "--dir", "service"], parent)).code, 0);
    await fs.rm(path.join(parent, "service", "ACCEPTANCE.md"));
    await fs.writeFile(path.join(parent, "service", ".env"), "MAPPLS_STATIC_KEY=mmp_prod_not-a-real-but-forbidden-key\n");
    const doctor = await invoke(["doctor", "service", "--json"], parent);
    const report = JSON.parse(doctor.stdout);
    assert.equal(doctor.code, 1);
    assert.equal(report.status, "fail");
    assert.match(report.failures.join(" "), /ACCEPTANCE\.md/);
    assert.match(report.failures.join(" "), /Credential hygiene/);
  } finally { await fs.rm(parent, { recursive: true, force: true }); }
});

test("starter path validator rejects traversal and absolute paths", () => {
  assert.equal(isSafeRelativePath("src/main.mjs"), true);
  assert.equal(isSafeRelativePath("../owned.txt"), false);
  assert.equal(isSafeRelativePath("src/../../owned.txt"), false);
  assert.equal(isSafeRelativePath("/tmp/owned.txt"), false);
});

test("CLI never accepts a credential option", async () => {
  const result = await invoke(["search", "route", "--token", "secret"]);
  assert.equal(result.code, 1);
  assert.match(result.stderr, /unknown option/i);
  assert.doesNotMatch(result.stdout, /secret/);
});
