#!/usr/bin/env node

/**
 * Feed verification tests for clawsec-suite.
 *
 * Tests cover:
 * - Signature verification success/failure/tampered cases
 * - Checksum manifest verification success/failure/tampered cases
 * - Fail-closed behavior when signatures are missing/invalid
 * - Temporary compatibility flag behavior
 *
 * Run: node skills/clawsec-suite/test/feed_verification.test.mjs
 */

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

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LIB_PATH = path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib");

// Dynamic import to ensure we test the actual module
const { verifySignedPayload, loadLocalFeed, isValidFeedPayload } = await import(
  `${LIB_PATH}/feed.mjs`
);

let tempDirCleanup;

function createValidFeed() {
  return JSON.stringify(
    {
      version: "1.0.0",
      updated: "2026-02-08T12:00:00Z",
      advisories: [
        {
          id: "TEST-001",
          severity: "high",
          affected: ["test-skill@1.0.0"],
        },
      ],
    },
    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,
  );
}

// -----------------------------------------------------------------------------
// Test: verifySignedPayload - valid signature
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_ValidSignature() {
  const testName = "verifySignedPayload: valid signature passes";
  try {
    const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
    const payload = "test payload content";
    const signature = signPayload(payload, privateKeyPem);

    const result = verifySignedPayload(payload, signature, publicKeyPem);

    if (result === true) {
      pass(testName);
    } else {
      fail(testName, "Expected true, got false");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: verifySignedPayload - invalid signature
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_InvalidSignature() {
  const testName = "verifySignedPayload: invalid signature fails";
  try {
    const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
    const payload = "test payload content";
    const signature = signPayload(payload, privateKeyPem);

    // Tamper with payload
    const tamperedPayload = "TAMPERED payload content";
    const result = verifySignedPayload(tamperedPayload, signature, publicKeyPem);

    if (result === false) {
      pass(testName);
    } else {
      fail(testName, "Expected false for tampered payload, got true");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: verifySignedPayload - wrong key
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_WrongKey() {
  const testName = "verifySignedPayload: wrong key fails";
  try {
    const keyPair1 = generateEd25519KeyPair();
    const keyPair2 = generateEd25519KeyPair();
    const payload = "test payload content";
    const signature = signPayload(payload, keyPair1.privateKeyPem);

    // Verify with different public key
    const result = verifySignedPayload(payload, signature, keyPair2.publicKeyPem);

    if (result === false) {
      pass(testName);
    } else {
      fail(testName, "Expected false for wrong key, got true");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: verifySignedPayload - malformed signature
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_MalformedSignature() {
  const testName = "verifySignedPayload: malformed signature fails";
  try {
    const { publicKeyPem } = generateEd25519KeyPair();
    const payload = "test payload content";

    const result = verifySignedPayload(payload, "not-valid-base64!!!", publicKeyPem);

    if (result === false) {
      pass(testName);
    } else {
      fail(testName, "Expected false for malformed signature, got true");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: verifySignedPayload - empty signature
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_EmptySignature() {
  const testName = "verifySignedPayload: empty signature fails";
  try {
    const { publicKeyPem } = generateEd25519KeyPair();
    const payload = "test payload content";

    const result = verifySignedPayload(payload, "", publicKeyPem);

    if (result === false) {
      pass(testName);
    } else {
      fail(testName, "Expected false for empty signature, got true");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: verifySignedPayload - JSON-wrapped signature format
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_JsonWrappedSignature() {
  const testName = "verifySignedPayload: JSON-wrapped signature passes";
  try {
    const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
    const payload = "test payload content";
    const signatureBase64 = signPayload(payload, privateKeyPem);
    const jsonWrapped = JSON.stringify({ signature: signatureBase64 });

    const result = verifySignedPayload(payload, jsonWrapped, publicKeyPem);

    if (result === true) {
      pass(testName);
    } else {
      fail(testName, "Expected true for JSON-wrapped signature, got false");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: loadLocalFeed - valid signed feed
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_ValidSignedFeed() {
  const testName = "loadLocalFeed: valid signed feed loads successfully";
  try {
    const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
    const feedContent = createValidFeed();
    const feedSignature = signPayload(feedContent, privateKeyPem);

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

    // Write files
    const feedPath = path.join(globalThis.__testTempDir, "feed.json");
    const sigPath = path.join(globalThis.__testTempDir, "feed.json.sig");
    const checksumPath = path.join(globalThis.__testTempDir, "checksums.json");
    const checksumSigPath = path.join(globalThis.__testTempDir, "checksums.json.sig");
    const keyPath = path.join(globalThis.__testTempDir, "feed-signing-public.pem");

    await fs.writeFile(feedPath, feedContent);
    await fs.writeFile(sigPath, feedSignature + "\n");
    await fs.writeFile(checksumPath, checksumManifest);
    await fs.writeFile(checksumSigPath, checksumSignature + "\n");
    await fs.writeFile(keyPath, publicKeyPem);

    const feed = await loadLocalFeed(feedPath, {
      signaturePath: sigPath,
      checksumsPath: checksumPath,
      checksumsSignaturePath: checksumSigPath,
      publicKeyPem,
      verifyChecksumManifest: true,
      checksumPublicKeyEntry: "feed-signing-public.pem",
    });

    if (feed && feed.version === "1.0.0" && feed.advisories.length === 1) {
      pass(testName);
    } else {
      fail(testName, "Feed did not load with expected content");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: loadLocalFeed - supports advisories/* checksum keys
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_AdvisoriesPrefixedChecksumKeys() {
  const testName = "loadLocalFeed: advisories/* checksum keys are accepted";
  try {
    const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
    const feedContent = createValidFeed();
    const feedSignature = signPayload(feedContent, privateKeyPem);

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

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

    const feedPath = path.join(advisoriesDir, "feed.json");
    const sigPath = path.join(advisoriesDir, "feed.json.sig");
    const checksumPath = path.join(advisoriesDir, "checksums.json");
    const checksumSigPath = path.join(advisoriesDir, "checksums.json.sig");
    const keyPath = path.join(advisoriesDir, "feed-signing-public.pem");

    await fs.writeFile(feedPath, feedContent);
    await fs.writeFile(sigPath, feedSignature + "\n");
    await fs.writeFile(checksumPath, checksumManifest);
    await fs.writeFile(checksumSigPath, checksumSignature + "\n");
    await fs.writeFile(keyPath, publicKeyPem);

    const feed = await loadLocalFeed(feedPath, {
      signaturePath: sigPath,
      checksumsPath: checksumPath,
      checksumsSignaturePath: checksumSigPath,
      publicKeyPem,
      verifyChecksumManifest: true,
      checksumPublicKeyEntry: path.basename(keyPath),
    });

    if (feed && feed.version === "1.0.0" && feed.advisories.length === 1) {
      pass(testName);
    } else {
      fail(testName, "Feed did not load with advisories/* checksum keys");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: loadLocalFeed - tampered feed fails (fail-closed)
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_TamperedFeedFails() {
  const testName = "loadLocalFeed: tampered feed fails (fail-closed)";
  try {
    const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
    const feedContent = createValidFeed();
    const feedSignature = signPayload(feedContent, privateKeyPem);

    // Tamper with feed after signing
    const tamperedFeed = feedContent.replace("TEST-001", "TAMPERED-001");

    const feedPath = path.join(globalThis.__testTempDir, "tampered-feed.json");
    const sigPath = path.join(globalThis.__testTempDir, "tampered-feed.json.sig");

    await fs.writeFile(feedPath, tamperedFeed);
    await fs.writeFile(sigPath, feedSignature + "\n");

    let didFail = false;
    try {
      await loadLocalFeed(feedPath, {
        signaturePath: sigPath,
        publicKeyPem,
        verifyChecksumManifest: false,
      });
    } catch {
      didFail = true;
    }

    if (didFail) {
      pass(testName);
    } else {
      fail(testName, "Expected failure for tampered feed, but it loaded");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: loadLocalFeed - missing signature fails (fail-closed)
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_MissingSignatureFails() {
  const testName = "loadLocalFeed: missing signature fails (fail-closed)";
  try {
    const { publicKeyPem } = generateEd25519KeyPair();
    const feedContent = createValidFeed();

    const feedPath = path.join(globalThis.__testTempDir, "nosig-feed.json");
    const sigPath = path.join(globalThis.__testTempDir, "nosig-feed.json.sig");

    await fs.writeFile(feedPath, feedContent);
    // Don't write signature file

    let didFail = false;
    try {
      await loadLocalFeed(feedPath, {
        signaturePath: sigPath,
        publicKeyPem,
        verifyChecksumManifest: false,
      });
    } catch {
      didFail = true;
    }

    if (didFail) {
      pass(testName);
    } else {
      fail(testName, "Expected failure for missing signature, but it loaded");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: loadLocalFeed - allowUnsigned bypasses verification
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_AllowUnsignedBypasses() {
  const testName = "loadLocalFeed: allowUnsigned=true bypasses verification";
  try {
    const feedContent = createValidFeed();

    const feedPath = path.join(globalThis.__testTempDir, "unsigned-feed.json");
    await fs.writeFile(feedPath, feedContent);

    const feed = await loadLocalFeed(feedPath, {
      allowUnsigned: true,
      verifyChecksumManifest: false,
    });

    if (feed && feed.version === "1.0.0") {
      pass(testName);
    } else {
      fail(testName, "Feed did not load with allowUnsigned=true");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: loadLocalFeed - checksum mismatch fails
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_ChecksumMismatchFails() {
  const testName = "loadLocalFeed: checksum mismatch fails";
  try {
    const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
    const feedContent = createValidFeed();
    const feedSignature = signPayload(feedContent, privateKeyPem);

    // Create checksum manifest with WRONG hash
    const badChecksumManifest = JSON.stringify(
      {
        schema_version: "1.0",
        algorithm: "sha256",
        files: {
          "feed.json": "0".repeat(64), // Wrong hash
          "feed.json.sig":
            crypto.createHash("sha256").update(feedSignature + "\n").digest("hex"),
        },
      },
      null,
      2,
    );
    const checksumSignature = signPayload(badChecksumManifest, privateKeyPem);

    const feedPath = path.join(globalThis.__testTempDir, "badcs-feed.json");
    const sigPath = path.join(globalThis.__testTempDir, "badcs-feed.json.sig");
    const checksumPath = path.join(globalThis.__testTempDir, "badcs-checksums.json");
    const checksumSigPath = path.join(globalThis.__testTempDir, "badcs-checksums.json.sig");

    await fs.writeFile(feedPath, feedContent);
    await fs.writeFile(sigPath, feedSignature + "\n");
    await fs.writeFile(checksumPath, badChecksumManifest);
    await fs.writeFile(checksumSigPath, checksumSignature + "\n");

    let didFail = false;
    try {
      await loadLocalFeed(feedPath, {
        signaturePath: sigPath,
        checksumsPath: checksumPath,
        checksumsSignaturePath: checksumSigPath,
        publicKeyPem,
        verifyChecksumManifest: true,
      });
    } catch {
      didFail = true;
    }

    if (didFail) {
      pass(testName);
    } else {
      fail(testName, "Expected failure for checksum mismatch, but it loaded");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: isValidFeedPayload - valid feed
// -----------------------------------------------------------------------------
async function testIsValidFeedPayload_Valid() {
  const testName = "isValidFeedPayload: valid feed passes";
  try {
    const feed = {
      version: "1.0.0",
      advisories: [
        {
          id: "TEST-001",
          severity: "high",
          affected: ["test-skill@1.0.0"],
        },
      ],
    };

    if (isValidFeedPayload(feed)) {
      pass(testName);
    } else {
      fail(testName, "Expected valid feed to pass validation");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: isValidFeedPayload - missing version fails
// -----------------------------------------------------------------------------
async function testIsValidFeedPayload_MissingVersion() {
  const testName = "isValidFeedPayload: missing version fails";
  try {
    const feed = {
      advisories: [
        {
          id: "TEST-001",
          severity: "high",
          affected: [],
        },
      ],
    };

    if (!isValidFeedPayload(feed)) {
      pass(testName);
    } else {
      fail(testName, "Expected feed without version to fail validation");
    }
  } catch (error) {
    fail(testName, error);
  }
}

// -----------------------------------------------------------------------------
// Test: isValidFeedPayload - advisory missing id fails
// -----------------------------------------------------------------------------
async function testIsValidFeedPayload_AdvisoryMissingId() {
  const testName = "isValidFeedPayload: advisory missing id fails";
  try {
    const feed = {
      version: "1.0.0",
      advisories: [
        {
          severity: "high",
          affected: [],
        },
      ],
    };

    if (!isValidFeedPayload(feed)) {
      pass(testName);
    } else {
      fail(testName, "Expected advisory without id to fail validation");
    }
  } catch (error) {
    fail(testName, error);
  }
}

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

  const tempDir = await createTempDir();
  tempDirCleanup = tempDir.cleanup;

  // Store temp dir path in module scope for tests to access
  globalThis.__testTempDir = tempDir.path;

  try {
    // Signature verification tests
    await testVerifySignedPayload_ValidSignature();
    await testVerifySignedPayload_InvalidSignature();
    await testVerifySignedPayload_WrongKey();
    await testVerifySignedPayload_MalformedSignature();
    await testVerifySignedPayload_EmptySignature();
    await testVerifySignedPayload_JsonWrappedSignature();

    // Local feed loading tests
    await testLoadLocalFeed_ValidSignedFeed();
    await testLoadLocalFeed_AdvisoriesPrefixedChecksumKeys();
    await testLoadLocalFeed_TamperedFeedFails();
    await testLoadLocalFeed_MissingSignatureFails();
    await testLoadLocalFeed_AllowUnsignedBypasses();
    await testLoadLocalFeed_ChecksumMismatchFails();

    // Feed payload validation tests
    await testIsValidFeedPayload_Valid();
    await testIsValidFeedPayload_MissingVersion();
    await testIsValidFeedPayload_AdvisoryMissingId();
  } finally {
    await tempDirCleanup();
  }

  report();
  exitWithResults();
}

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