// v3.7.0 PR4 — GitHub repo metadata invariant.
//
// Background. The README + npm description lead with "long-term memory for
// AI agents" (since v3.6.3). The GitHub repo's About description + Topics
// were updated to match out-of-band via `gh api`. But that metadata lives
// only on GitHub — no CI check catches drift if someone (or a future
// automation) silently rewrites it. This test pulls the current state via
// `gh api repos/oomkapwn/enquire-mcp` and asserts the positioning + the
// presence of the 8 hype topics shipped with v3.6.3.
//
// Skip behavior. The test runs only when:
//   1. `gh` is on PATH and authenticated (typically: in CI via GITHUB_TOKEN,
//      or local devs who ran `gh auth login`).
//   2. Network is reachable.
// Otherwise the test gracefully `it.skip`s with a one-line explanation.
// This avoids local devs / offline CI variants failing on auth they don't
// have. The skip is INTENTIONALLY non-failing — see the v3.6.4 method note
// "Invariant test without negative-control" for why we wouldn't accept a
// silent always-pass; here the skip is explicit, the assertion is real.

import { execSync, spawnSync } from "node:child_process";
import { describe, expect, it } from "vitest";

const REPO = "oomkapwn/enquire-mcp";
// v3.7.9 — REQUIRED_TOPICS updated to reflect the v3.7.8 positioning calibration:
//   - openclaw added (restored after v3.6.3 dropped it to fit hype keywords)
//   - context-engineering removed (swapped for openclaw in v3.7.8 Topics rebalance)
// Per round-11 audit: the test was carrying the v3.7.0 8-topic shortlist
// while the v3.7.8 metadata had moved on. Bringing the invariant up to date.
const REQUIRED_TOPICS = [
  "ai-memory",
  "agent-memory",
  "llm-memory",
  "long-term-memory",
  "claude-memory",
  "second-brain",
  "openclaw",
  "obsidian-mcp"
];
// v3.7.8 — About now leads with "The most advanced Obsidian MCP" credential
// followed by the value prop. v3.7.9 invariant matches this new lead phrase
// (the v3.7.0 invariant matched "Memory layer for AI agents"; v3.7.8 changed
// the About copy out-of-band via gh api, and round-11 caught the test drift).
const ABOUT_LEADS_WITH = /^The most advanced Obsidian MCP/i;

function ghIsAvailable(): boolean {
  try {
    // `gh auth status` exits 0 when authenticated, non-zero otherwise. We
    // pipe stderr → /dev/null to keep the test output clean.
    execSync("gh auth status", { stdio: "ignore" });
    return true;
  } catch {
    return false;
  }
}

interface RepoMeta {
  description: string;
  topics: string[];
}

function fetchRepoMeta(): RepoMeta | null {
  const res = spawnSync("gh", ["api", `repos/${REPO}`, "--jq", "{description, topics}"], {
    encoding: "utf8",
    timeout: 15_000
  });
  if (res.status !== 0) return null;
  try {
    return JSON.parse(res.stdout) as RepoMeta;
  } catch {
    return null;
  }
}

/**
 * v3.7.4 — extracted assertion helpers for negative-control coverage.
 * Per CLAUDE.md anti-pattern "Invariant test without negative-control —
 * Rule since v3.6.4": every invariant test must have a sibling that
 * fails when the invariant is violated. v3.7.0 shipped this invariant
 * with assertions inlined, which made negative-control impossible.
 * v3.7.4 extracts the logic so we can prove the analyzer flags drift.
 */
function validateAboutLeadsWith(description: string): boolean {
  return ABOUT_LEADS_WITH.test(description ?? "");
}
function findMissingTopics(topics: string[]): string[] {
  const set = new Set(topics ?? []);
  return REQUIRED_TOPICS.filter((t) => !set.has(t));
}

describe("GitHub repo metadata invariant (v3.7.0 + v3.7.4 negative-control)", () => {
  // Always use `it` (not `it.skip`) so the total `it()` count is constant
  // across local-with-gh-auth and CI-without-gh-auth environments. The
  // `tests/docs-consistency.test.ts` regex counts `^\s*it\(` declarations
  // for its test-count claim; conditional `it.skip` would fluctuate the
  // count. Instead, each test early-returns when `gh` isn't available —
  // the test "passes" without asserting (treated as a no-op skip).
  const available = ghIsAvailable();

  it("repo About description leads with 'Memory layer for AI agents'", () => {
    if (!available) {
      // v3.7.13 L4 — CI now sets `GH_TOKEN: ${{ github.token }}` so this
      // branch only fires in local dev without auth. Production CI runs
      // assert against the live repo and would fail loud on drift.
      console.warn("[github-metadata] `gh` not authenticated; skipping (set GH_TOKEN env or GITHUB_TOKEN for CI).");
      return;
    }
    const meta = fetchRepoMeta();
    // v3.7.13 L4 — fail loud on API failure when `gh` is available. Pre-3.7.13
    // we no-op'd here, which let CI count this as "passed" even if rate-limit
    // / network / token-scope blocked the fetch. If `gh` is available but the
    // API fails, that's a real signal worth surfacing.
    expect(
      meta,
      "gh api call failed despite gh being available — check rate limit / network / token scope"
    ).not.toBeNull();
    if (!meta) return;
    expect(meta.description ?? "").toMatch(ABOUT_LEADS_WITH);
  });

  it("repo Topics include the 8 v3.6.3 hype keywords", () => {
    if (!available) {
      console.warn("[github-metadata] `gh` not authenticated; skipping (set GH_TOKEN env or GITHUB_TOKEN for CI).");
      return;
    }
    const meta = fetchRepoMeta();
    expect(
      meta,
      "gh api call failed despite gh being available — check rate limit / network / token scope"
    ).not.toBeNull();
    if (!meta) return;
    const missing = findMissingTopics(meta.topics);
    expect(missing, `Missing topics: ${missing.join(", ")}`).toEqual([]);
  });

  // v3.7.4 — NEGATIVE-CONTROL siblings. The 2 production tests above pass
  // when gh metadata matches the expected positioning. Without the negative
  // control, if `ABOUT_LEADS_WITH` regex or `REQUIRED_TOPICS` array broke,
  // the production tests would silent-pass even on bad input. These tests
  // call the extracted pure functions on KNOWN-BAD inputs and assert the
  // analyzer correctly flags them.
  //
  // Per CLAUDE.md anti-pattern "Invariant test without negative-control —
  // Rule since v3.6.4". v3.7.0 shipped this invariant without negative-
  // control (oversight); v3.7.4 closes the gap.
  describe("NEGATIVE-CONTROL: analyzers detect drift on synthetic bad inputs (v3.7.4)", () => {
    it("validateAboutLeadsWith rejects descriptions that don't lead with the canonical phrase", () => {
      // v3.7.9 — canonical About lead is now "The most advanced Obsidian MCP"
      // (was "Memory layer for AI agents" before v3.7.8). Update positive +
      // negative cases accordingly.
      expect(validateAboutLeadsWith("The most advanced Obsidian MCP — long-term memory for AI agents")).toBe(true);
      // Case-insensitive — same canonical phrase, lowercase.
      expect(validateAboutLeadsWith("the most advanced obsidian mcp — built")).toBe(true);
      // Negative cases — analyzer MUST flag these.
      expect(validateAboutLeadsWith("Memory layer for AI agents — built on Obsidian.")).toBe(false);
      expect(validateAboutLeadsWith("The most advanced MCP server for Obsidian vaults.")).toBe(false); // "MCP server for Obsidian" ≠ "Obsidian MCP"
      expect(validateAboutLeadsWith("")).toBe(false);
      expect(validateAboutLeadsWith("Long-term memory for AI agents")).toBe(false); // pre-v3.7.8 phrasing
    });

    it("findMissingTopics returns all required topics when given empty input", () => {
      const missing = findMissingTopics([]);
      expect(missing.length).toBe(REQUIRED_TOPICS.length);
      // Spot-check that each required topic is in the missing list.
      for (const required of REQUIRED_TOPICS) {
        expect(missing).toContain(required);
      }
    });

    it("findMissingTopics returns subset when given partial topic list", () => {
      // Pass only 3 of 8 required → 5 should be reported missing.
      const partial = REQUIRED_TOPICS.slice(0, 3);
      const missing = findMissingTopics(partial);
      expect(missing.length).toBe(REQUIRED_TOPICS.length - 3);
      // The 3 we passed must NOT be in missing.
      for (const passed of partial) {
        expect(missing).not.toContain(passed);
      }
    });

    it("findMissingTopics returns [] when all required topics are present (positive control)", () => {
      // Mix required topics with some unrelated extras — analyzer should
      // ignore the extras and report no missing.
      const full = [...REQUIRED_TOPICS, "extra-1", "extra-2"];
      expect(findMissingTopics(full)).toEqual([]);
    });
  });
});
