import { existsSync, readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

import { describe, expect, it } from "vitest";

import { parseRunnerManifestYaml, validateRunnerManifest } from "./cli-parser/index.js";

import { officialSkillVisibleForCatalog } from "./skill-refs.js";

const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const paymentGraphStageOwners: Readonly<Record<string, string>> = {
  "charge-challenge": "charge",
  "charge-price": "charge",
  "charge-verify": "charge",
  "pay-fulfill-rail": "spend",
  "pay-quote": "spend",
  "pay-recover": "spend",
  "pay-reserve": "spend",
  "refund-quote": "refund",
  "refund-recover": "refund",
  "refund-reserve": "refund",
};
const paymentHarnessFixtures = [
  "mock-charge",
  "mock-pay",
  "mock-refund",
];
const paymentRuntimePaths = [
  "mpp-charge",
  "mpp-pay",
  "mpp-refund",
  "stripe-charge",
  "stripe-refund",
];
const issueToPrGraphStageOwners: Readonly<Record<string, string>> = {
  scafld: "issue-to-pr",
};

describe("official skill catalog exposure", () => {
  it("hides non-catalog official skills unless the dev catalog is explicitly enabled", () => {
    expect(officialSkillVisibleForCatalog("runx/mock-pay", {})).toBe(false);
    expect(officialSkillVisibleForCatalog("runx/x402-pay", {})).toBe(true);
    expect(officialSkillVisibleForCatalog("runx/stripe-pay", {})).toBe(true);
    expect(officialSkillVisibleForCatalog("runx/issue-to-pr", {})).toBe(true);
    expect(officialSkillVisibleForCatalog("runx/research", {})).toBe(true);
    expect(officialSkillVisibleForCatalog("runx/stripe-charge", {})).toBe(false);
    expect(
      officialSkillVisibleForCatalog("runx/mock-pay", {
        RUNX_DEV_CATALOG: "1",
      }),
    ).toBe(true);
  });

  it("keeps implemented catalog skills visible", () => {
    for (const skill of publicOfficialCatalogSkills()) {
      expect(officialSkillVisibleForCatalog(`runx/${skill}`, {}), skill).toBe(true);
    }
  });

  it("keeps catalog visibility explicit in first-party runner manifests", () => {
    const allSkills = readdirSync(path.join(repoRoot, "skills"), { withFileTypes: true })
      .filter((entry) => entry.isDirectory())
      .filter((entry) => !entry.name.startsWith("."))
      .filter((entry) => {
        const skillDir = path.join(repoRoot, "skills", entry.name);
        return existsSync(path.join(skillDir, "SKILL.md")) && existsSync(path.join(skillDir, "X.yaml"));
      })
      .map((entry) => entry.name)
      .sort();
    const expectedPublic = new Set(publicOfficialCatalogSkills());
    const actualPublic = allSkills.filter((skill) => catalogVisibility(skill) === "public");

    expect(actualPublic).toEqual([...expectedPublic].sort());
    for (const skill of allSkills) {
      expect(catalogVisibility(skill), skill).toBe(expectedPublic.has(skill) ? "public" : "internal");
      expect(catalogRole(skill), skill).toBeTruthy();
    }
  });

  it("keeps payment lifecycle internals and rail fixtures out of the catalog with explicit roles", () => {
    for (const [stage, owner] of Object.entries(paymentGraphStageOwners)) {
      expect(existsSync(path.join(repoRoot, "skills", owner, "graph", stage, "X.yaml")), stage).toBe(true);
      expect(existsSync(path.join(repoRoot, "skills", stage)), stage).toBe(false);
    }
    for (const [stage, owner] of Object.entries(issueToPrGraphStageOwners)) {
      expect(existsSync(path.join(repoRoot, "skills", owner, "graph", stage, "X.yaml")), stage).toBe(true);
      expect(existsSync(path.join(repoRoot, "skills", stage)), stage).toBe(false);
    }
    for (const skill of paymentHarnessFixtures) {
      expect(catalogVisibility(skill), skill).toBe("internal");
      expect(catalogRole(skill), skill).toBe("harness-fixture");
    }
    for (const skill of paymentRuntimePaths) {
      expect(catalogVisibility(skill), skill).toBe("internal");
      expect(catalogRole(skill), skill).toBe("runtime-path");
    }
  });
});

function publicOfficialCatalogSkills(): readonly string[] {
  return readdirSync(path.join(repoRoot, "skills"), { withFileTypes: true })
    .filter((entry) => entry.isDirectory())
    .filter((entry) => !entry.name.startsWith("."))
    .filter((entry) => {
      const skillDir = path.join(repoRoot, "skills", entry.name);
      return existsSync(path.join(skillDir, "SKILL.md")) && existsSync(path.join(skillDir, "X.yaml"));
    })
    .map((entry) => entry.name)
    .filter((skill) => catalogVisibility(skill) === "public")
    .sort();
}

function catalogVisibility(skill: string): string | undefined {
  const manifestPath = path.join(repoRoot, "skills", skill, "X.yaml");
  const manifest = validateRunnerManifest(parseRunnerManifestYaml(readFileSync(manifestPath, "utf8")));
  return manifest.catalog?.visibility;
}

function catalogRole(skill: string): string | undefined {
  const manifestPath = path.join(repoRoot, "skills", skill, "X.yaml");
  const manifest = validateRunnerManifest(parseRunnerManifestYaml(readFileSync(manifestPath, "utf8")));
  return manifest.catalog?.role;
}
