#!/usr/bin/env node

/**
 * Guarded skill install tests for clawsec-suite.
 *
 * Tests cover:
 * - Conservative matching when version is omitted
 * - Precise version matching when version is provided
 * - Exit code 42 for advisory match requiring confirmation
 * - High-risk advisory detection
 *
 * Run: node skills/clawsec-suite/test/guarded_install.test.mjs
 */

import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import {
  pass,
  fail,
  report,
  exitWithResults,
  generateEd25519KeyPair,
  signPayload,
} from "./lib/test_harness.mjs";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT_PATH = path.resolve(__dirname, "..", "scripts", "guarded_skill_install.mjs");

let tempDir;

function createFeed(advisories) {
  return JSON.stringify(
    {
      version: "1.0.0",
      updated: "2026-02-08T12:00:00Z",
      advisories,
    },
    null,
    2,
  );
}

function createChecksumManifest(files) {
  const checksums = {};
  for (const [name, content] of Object.entries(files)) {
    checksums[name] = crypto.createHash("sha256").update(content).digest("hex");
  }
  return JSON.stringify(
    {
      schema_version: "1.0",
      algorithm: "sha256",
      files: checksums,
    },
    null,
    2,
  );
}

async function setupTestDir() {
  tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawsec-install-test-"));
}

async function cleanupTestDir() {
  if (tempDir) {
    await fs.rm(tempDir, { recursive: true, force: true });
  }
}

async function setupSignedFeed(advisories, keyPair) {
  const feedContent = createFeed(advisories);
  const feedSignature = signPayload(feedContent, keyPair.privateKeyPem);

  const checksumManifest = createChecksumManifest({
    "feed.json": feedContent,
    "feed.json.sig": feedSignature + "\n",
    "feed-signing-public.pem": keyPair.publicKeyPem,
  });
  const checksumSignature = signPayload(checksumManifest, keyPair.privateKeyPem);

  const advisoriesDir = path.join(tempDir, "advisories");
  await fs.mkdir(advisoriesDir, { recursive: true });

  await fs.writeFile(path.join(advisoriesDir, "feed.json"), feedContent);
  await fs.writeFile(path.join(advisoriesDir, "feed.json.sig"), feedSignature + "\n");
  await fs.writeFile(path.join(advisoriesDir, "checksums.json"), checksumManifest);
  await fs.writeFile(path.join(advisoriesDir, "checksums.json.sig"), checksumSignature + "\n");
  await fs.writeFile(path.join(advisoriesDir, "feed-signing-public.pem"), keyPair.publicKeyPem);

  return advisoriesDir;
}

function runGuardedInstall(args, env) {
  return new Promise((resolve) => {
    const proc = spawn("node", [SCRIPT_PATH, ...args], {
      env: { ...process.env, ...env },
      stdio: ["ignore", "pipe", "pipe"],
    });

    let stdout = "";
    let stderr = "";

    proc.stdout.on("data", (data) => {
      stdout += data.toString();
    });

    proc.stderr.on("data", (data) => {
      stderr += data.toString();
    });

    proc.on("close", (code) => {
      resolve({ code, stdout, stderr });
    });
  });
}

// -----------------------------------------------------------------------------
// Test: Conservative matching when version is omitted
// -----------------------------------------------------------------------------
async function testConservativeMatchingWithoutVersion() {
  const testName = "guarded_install: conservative matching without version triggers advisory";
  try {
    const keyPair = generateEd25519KeyPair();
    const advisoriesDir = await setupSignedFeed(
      [
        {
          id: "TEST-001",
          severity: "high",
          affected: ["test-skill@1.0.0", "test-skill@1.0.1"],
        },
      ],
      keyPair,
    );

    const result = await runGuardedInstall(["--skill", "test-skill", "--dry-run"], {
      CLAWSEC_LOCAL_FEED: path.join(advisoriesDir, "feed.json"),
      CLAWSEC_LOCAL_FEED_SIG: path.join(advisoriesDir, "feed.json.sig"),
      CLAWSEC_LOCAL_FEED_CHECKSUMS: path.join(advisoriesDir, "checksums.json"),
      CLAWSEC_LOCAL_FEED_CHECKSUMS_SIG: path.join(advisoriesDir, "checksums.json.sig"),
      CLAWSEC_FEED_PUBLIC_KEY: path.join(advisoriesDir, "feed-signing-public.pem"),
      CLAWSEC_FEED_URL: "file:///nonexistent", // Force local fallback
    });

    if (result.code === 42 && result.stdout.includes("Conservative")) {
      pass(testName);
    } else {
      fail(testName, `Expected exit 42 with conservative message, got ${result.code}: ${result.stdout}`);
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: Precise version matching
// -----------------------------------------------------------------------------
async function testPreciseVersionMatching() {
  const testName = "guarded_install: precise version matching only matches exact version";
  try {
    const keyPair = generateEd25519KeyPair();
    const advisoriesDir = await setupSignedFeed(
      [
        {
          id: "TEST-001",
          severity: "high",
          affected: ["test-skill@1.0.0"],
        },
      ],
      keyPair,
    );

    // Version 2.0.0 should NOT match advisory for 1.0.0
    const result = await runGuardedInstall(
      ["--skill", "test-skill", "--version", "2.0.0", "--dry-run"],
      {
        CLAWSEC_LOCAL_FEED: path.join(advisoriesDir, "feed.json"),
        CLAWSEC_LOCAL_FEED_SIG: path.join(advisoriesDir, "feed.json.sig"),
        CLAWSEC_LOCAL_FEED_CHECKSUMS: path.join(advisoriesDir, "checksums.json"),
        CLAWSEC_LOCAL_FEED_CHECKSUMS_SIG: path.join(advisoriesDir, "checksums.json.sig"),
        CLAWSEC_FEED_PUBLIC_KEY: path.join(advisoriesDir, "feed-signing-public.pem"),
        CLAWSEC_FEED_URL: "file:///nonexistent",
      },
    );

    if (result.code === 0 && !result.stdout.includes("Advisory matches")) {
      pass(testName);
    } else {
      fail(testName, `Expected exit 0 without match, got ${result.code}: ${result.stdout}`);
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: Version match triggers confirmation requirement
// -----------------------------------------------------------------------------
async function testVersionMatchTriggersConfirmation() {
  const testName = "guarded_install: matching version triggers exit 42";
  try {
    const keyPair = generateEd25519KeyPair();
    const advisoriesDir = await setupSignedFeed(
      [
        {
          id: "TEST-001",
          severity: "high",
          affected: ["test-skill@1.0.0"],
        },
      ],
      keyPair,
    );

    const result = await runGuardedInstall(
      ["--skill", "test-skill", "--version", "1.0.0", "--dry-run"],
      {
        CLAWSEC_LOCAL_FEED: path.join(advisoriesDir, "feed.json"),
        CLAWSEC_LOCAL_FEED_SIG: path.join(advisoriesDir, "feed.json.sig"),
        CLAWSEC_LOCAL_FEED_CHECKSUMS: path.join(advisoriesDir, "checksums.json"),
        CLAWSEC_LOCAL_FEED_CHECKSUMS_SIG: path.join(advisoriesDir, "checksums.json.sig"),
        CLAWSEC_FEED_PUBLIC_KEY: path.join(advisoriesDir, "feed-signing-public.pem"),
        CLAWSEC_FEED_URL: "file:///nonexistent",
      },
    );

    if (result.code === 42 && result.stdout.includes("Advisory matches")) {
      pass(testName);
    } else {
      fail(testName, `Expected exit 42 with advisory match, got ${result.code}: ${result.stdout}`);
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: --confirm-advisory allows proceeding
// -----------------------------------------------------------------------------
async function testConfirmAdvisoryAllowsProceeding() {
  const testName = "guarded_install: --confirm-advisory with --dry-run proceeds";
  try {
    const keyPair = generateEd25519KeyPair();
    const advisoriesDir = await setupSignedFeed(
      [
        {
          id: "TEST-001",
          severity: "high",
          affected: ["test-skill@1.0.0"],
        },
      ],
      keyPair,
    );

    const result = await runGuardedInstall(
      ["--skill", "test-skill", "--version", "1.0.0", "--confirm-advisory", "--dry-run"],
      {
        CLAWSEC_LOCAL_FEED: path.join(advisoriesDir, "feed.json"),
        CLAWSEC_LOCAL_FEED_SIG: path.join(advisoriesDir, "feed.json.sig"),
        CLAWSEC_LOCAL_FEED_CHECKSUMS: path.join(advisoriesDir, "checksums.json"),
        CLAWSEC_LOCAL_FEED_CHECKSUMS_SIG: path.join(advisoriesDir, "checksums.json.sig"),
        CLAWSEC_FEED_PUBLIC_KEY: path.join(advisoriesDir, "feed-signing-public.pem"),
        CLAWSEC_FEED_URL: "file:///nonexistent",
      },
    );

    if (result.code === 0 && result.stdout.includes("Dry run")) {
      pass(testName);
    } else {
      fail(testName, `Expected exit 0 with dry run message, got ${result.code}: ${result.stdout}`);
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: allowUnsigned bypass warning
// -----------------------------------------------------------------------------
async function testAllowUnsignedWarning() {
  const testName = "guarded_install: CLAWSEC_ALLOW_UNSIGNED_FEED shows warning";
  try {
    // Create unsigned feed (no signatures)
    const feedContent = createFeed([]);
    const feedPath = path.join(tempDir, "unsigned-feed.json");
    await fs.writeFile(feedPath, feedContent);

    const result = await runGuardedInstall(["--skill", "test-skill", "--dry-run"], {
      CLAWSEC_LOCAL_FEED: feedPath,
      CLAWSEC_ALLOW_UNSIGNED_FEED: "1",
      CLAWSEC_VERIFY_CHECKSUM_MANIFEST: "0",
      CLAWSEC_FEED_URL: "file:///nonexistent",
    });

    if (result.stderr.includes("CLAWSEC_ALLOW_UNSIGNED_FEED")) {
      pass(testName);
    } else {
      fail(testName, `Expected unsigned mode warning, got: ${result.stderr}`);
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: Missing signature fails without allowUnsigned
// -----------------------------------------------------------------------------
async function testMissingSignatureFails() {
  const testName = "guarded_install: missing signature fails without allowUnsigned";
  try {
    const feedContent = createFeed([]);
    const feedPath = path.join(tempDir, "nosig-feed.json");
    await fs.writeFile(feedPath, feedContent);

    const result = await runGuardedInstall(["--skill", "test-skill", "--dry-run"], {
      CLAWSEC_LOCAL_FEED: feedPath,
      CLAWSEC_FEED_URL: "file:///nonexistent",
    });

    if (result.code === 1) {
      pass(testName);
    } else {
      fail(testName, `Expected exit 1 for missing signature, got ${result.code}`);
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: $HOME path expansion for local feed paths
// -----------------------------------------------------------------------------
async function testHomeExpansionForLocalFeedPaths() {
  const testName = "guarded_install: expands $HOME in local feed env paths";
  try {
    const keyPair = generateEd25519KeyPair();
    await setupSignedFeed([], keyPair);

    const result = await runGuardedInstall(["--skill", "test-skill", "--dry-run"], {
      HOME: tempDir,
      CLAWSEC_LOCAL_FEED: "$HOME/advisories/feed.json",
      CLAWSEC_LOCAL_FEED_SIG: "$HOME/advisories/feed.json.sig",
      CLAWSEC_LOCAL_FEED_CHECKSUMS: "$HOME/advisories/checksums.json",
      CLAWSEC_LOCAL_FEED_CHECKSUMS_SIG: "$HOME/advisories/checksums.json.sig",
      CLAWSEC_FEED_PUBLIC_KEY: "$HOME/advisories/feed-signing-public.pem",
      CLAWSEC_FEED_URL: "file:///nonexistent",
    });

    if (result.code === 0 && result.stdout.includes("Advisory source: local:")) {
      pass(testName);
    } else {
      fail(testName, `Expected local feed success, got ${result.code}: ${result.stdout} ${result.stderr}`);
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: escaped home token is rejected
// -----------------------------------------------------------------------------
async function testEscapedHomeTokenRejected() {
  const testName = "guarded_install: escaped $HOME token is rejected";
  try {
    const result = await runGuardedInstall(["--skill", "test-skill", "--dry-run"], {
      CLAWSEC_LOCAL_FEED: "\\$HOME/advisories/feed.json",
    });

    if (result.code === 1 && result.stderr.includes("Unexpanded home token")) {
      pass(testName);
    } else {
      fail(testName, `Expected token validation error, got ${result.code}: ${result.stderr || result.stdout}`);
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Main test runner
// -----------------------------------------------------------------------------
async function runTests() {
  console.log("=== ClawSec Guarded Install Tests ===\n");

  await setupTestDir();

  try {
    await testConservativeMatchingWithoutVersion();
    await testPreciseVersionMatching();
    await testVersionMatchTriggersConfirmation();
    await testConfirmAdvisoryAllowsProceeding();
    await testAllowUnsignedWarning();
    await testMissingSignatureFails();
    await testHomeExpansionForLocalFeedPaths();
    await testEscapedHomeTokenRejected();
  } finally {
    await cleanupTestDir();
  }

  report();
  exitWithResults();
}

runTests().catch((error) => {
  console.error("Test runner failed:", error);
  process.exit(1);
});
