import { mkdtempSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { describe, it, expect, vi } from "vitest";
import {
  DPF_PLATFORM_SKILL_SOURCE_TYPE,
  LEGACY_SKILL_SOURCE_TYPE,
  discoverDpfPlatformSkillFiles,
  normalizeSkillFrontmatterForSeed,
  parseAllowedTools,
  parseFrontmatter,
  reconcileSkillAssignments,
  selectSkillSeedEntries,
  validateDpfPlatformSkillFrontmatter,
  writeSkillSeedConflictWarnings,
  type LoadedSkillSeedEntry,
} from "./seed-skills";

describe("seed-skills parseFrontmatter", () => {
  it("parses inline arrays", () => {
    const raw = `---
name: foo
description: test
category: storefront
assignTo: ["coo", "admin-assistant"]
allowedTools: [tool_a, tool_b]
composesFrom: []
---

Body.`;
    const { frontmatter } = parseFrontmatter(raw);
    expect(frontmatter.assignTo).toEqual(["coo", "admin-assistant"]);
    expect(frontmatter.allowedTools).toEqual(["tool_a", "tool_b"]);
    expect(frontmatter.composesFrom).toEqual([]);
  });

  it("parses block-style lists (the extract-brand-design-system format)", () => {
    const raw = `---
name: extract-brand-design-system
description: test
category: storefront
allowedTools:
  - extract_brand_design_system
  - analyze_public_website_branding
  - analyze_brand_document
composesFrom: []
---

Body.`;
    const { frontmatter } = parseFrontmatter(raw);
    expect(frontmatter.allowedTools).toEqual([
      "extract_brand_design_system",
      "analyze_public_website_branding",
      "analyze_brand_document",
    ]);
  });

  it("parses mixed scalars, booleans, null", () => {
    const raw = `---
name: mixed
description: "quoted"
userInvocable: true
agentInvocable: false
triggerPattern: null
---

Body.`;
    const { frontmatter } = parseFrontmatter(raw);
    expect(frontmatter.name).toBe("mixed");
    expect(frontmatter.description).toBe("quoted");
    expect(frontmatter.userInvocable).toBe(true);
    expect(frontmatter.agentInvocable).toBe(false);
    expect(frontmatter.triggerPattern).toBe(null);
  });

  it("never produces a non-array for known array fields when value is empty", () => {
    const raw = `---
name: empty-lists
description: test
allowedTools:
composesFrom:
---

Body.`;
    const { frontmatter } = parseFrontmatter(raw);
    // An empty key with no block list below is a scalar "" — callers must
    // defend with Array.isArray. But a key with a block list (even zero
    // items due to no "-" lines) should at least not become anything Prisma
    // chokes on. Keeping this as a regression guard.
    expect(Array.isArray(frontmatter.allowedTools) || frontmatter.allowedTools === "").toBe(true);
  });
});

describe("reconcileSkillAssignments", () => {
  it("removes obsolete system-seeded assignments when assignTo changes", async () => {
    const skillAssignment = {
      findMany: vi.fn().mockResolvedValue([
        { agentId: "portfolio-advisor", assignedBy: "system-seed" },
        { agentId: "external-catalog-scout", assignedBy: "system-seed" },
        { agentId: "coo", assignedBy: "human-admin" },
      ]),
      create: vi.fn(),
      delete: vi.fn(),
    };

    const result = await reconcileSkillAssignments(
      { skillAssignment } as never,
      "scout-external-catalogs",
      ["external-catalog-scout"],
    );

    expect(skillAssignment.delete).toHaveBeenCalledWith({
      where: {
        skillId_agentId: {
          skillId: "scout-external-catalogs",
          agentId: "portfolio-advisor",
        },
      },
    });
    expect(skillAssignment.delete).toHaveBeenCalledTimes(1);
    expect(skillAssignment.create).not.toHaveBeenCalled();
    expect(result).toEqual({ created: 0, removed: 1 });
  });
});

describe("seed-skills dpf-platform loader", () => {
  it("discovers plugin SKILL.md files under slug directories", () => {
    const root = mkdtempSync(join(tmpdir(), "dpf-skill-pack-"));
    const skillDir = join(root, "skills", "dpf-example");
    mkdirSync(skillDir, { recursive: true });
    writeFileSync(join(skillDir, "SKILL.md"), "---\nname: dpf-example\n---\n\nBody.\n");

    expect(discoverDpfPlatformSkillFiles(join(root, "skills"))).toEqual([
      {
        category: "dpf-platform",
        filePath: join(skillDir, "SKILL.md"),
        sourceType: DPF_PLATFORM_SKILL_SOURCE_TYPE,
      },
    ]);
  });

  it("maps Agent Skills standard fields onto coworker seed fields", () => {
    const raw = `---
name: dpf-agent-standard-only
description: test standard mapping
disable-model-invocation: false
user-invocable: true
allowed-tools: Bash(git log *) Grep mcp__dpf__wiki_query
category: governance
assignTo: ["*"]
---

Body.`;

    const { frontmatter } = parseFrontmatter(raw);
    const normalized = normalizeSkillFrontmatterForSeed({
      frontmatter,
      raw,
      category: "governance",
      sourceType: DPF_PLATFORM_SKILL_SOURCE_TYPE,
    });

    expect(normalized.skillId).toBe("dpf-agent-standard-only");
    expect(normalized.skillDefinition.sourceType).toBe(DPF_PLATFORM_SKILL_SOURCE_TYPE);
    expect(normalized.skillDefinition.userInvocable).toBe(true);
    expect(normalized.skillDefinition.agentInvocable).toBe(true);
    expect(normalized.skillDefinition.allowedTools).toEqual([
      "Bash",
      "Grep",
      "mcp__dpf__wiki_query",
    ]);
  });

  it("selects plugin SKILL.md over legacy .skill.md and reports a warning", () => {
    const legacy = {
      skillId: "dpf-same",
      category: "ops",
      filePath: "skills/ops/dpf-same.skill.md",
      raw: "legacy",
      sourceType: LEGACY_SKILL_SOURCE_TYPE,
      frontmatter: { name: "dpf-same", description: "legacy" },
    } satisfies LoadedSkillSeedEntry;
    const plugin = {
      skillId: "dpf-same",
      category: "dpf-platform",
      filePath: "packages/dpf-skill-pack/skills/dpf-same/SKILL.md",
      raw: "plugin",
      sourceType: DPF_PLATFORM_SKILL_SOURCE_TYPE,
      frontmatter: { name: "dpf-same", description: "plugin" },
    } satisfies LoadedSkillSeedEntry;

    const { selected, conflicts } = selectSkillSeedEntries([legacy, plugin]);

    expect(selected).toEqual([plugin]);
    expect(conflicts).toEqual([
      {
        skillId: "dpf-same",
        legacyPath: legacy.filePath,
        pluginPath: plugin.filePath,
      },
    ]);
  });

  it("writes deterministic SkillSeedWarning rows for plugin-over-legacy conflicts", async () => {
    const skillSeedWarning = {
      upsert: vi.fn().mockResolvedValue({}),
    };

    await writeSkillSeedConflictWarnings(
      { skillSeedWarning } as never,
      [
        {
          skillId: "dpf-same",
          legacyPath: "skills/ops/dpf-same.skill.md",
          pluginPath: "packages/dpf-skill-pack/skills/dpf-same/SKILL.md",
        },
      ],
    );

    expect(skillSeedWarning.upsert).toHaveBeenCalledWith({
      where: { warningId: "plugin-overrides-legacy:dpf-same" },
      update: expect.objectContaining({
        resolvedAt: null,
        warningType: "plugin-overrides-legacy",
      }),
      create: expect.objectContaining({
        warningId: "plugin-overrides-legacy:dpf-same",
        skillId: "dpf-same",
        warningType: "plugin-overrides-legacy",
      }),
    });
  });
});

describe("dpf-platform mirror-field invariant", () => {
  it("keeps Agent Skills fields and DPF coworker fields non-contradictory", () => {
    const pluginSkillsDir = join(__dirname, "..", "..", "..", "packages", "dpf-skill-pack", "skills");
    const skillFiles = readdirSync(pluginSkillsDir, { withFileTypes: true })
      .filter((entry) => entry.isDirectory())
      .map((entry) => join(pluginSkillsDir, entry.name, "SKILL.md"));

    expect(skillFiles.length).toBeGreaterThan(0);

    const issues = skillFiles.flatMap((filePath) => {
      const raw = readFileSync(filePath, "utf-8");
      const { frontmatter } = parseFrontmatter(raw);
      return validateDpfPlatformSkillFrontmatter(frontmatter, filePath);
    });

    expect(issues).toEqual([]);
  });

  it("has no dangling composesFrom — every slug resolves to a pack skill (no bare upstream slugs)", () => {
    const pluginSkillsDir = join(__dirname, "..", "..", "..", "packages", "dpf-skill-pack", "skills");
    const frontmatters = readdirSync(pluginSkillsDir, { withFileTypes: true })
      .filter((entry) => entry.isDirectory())
      .map((entry) => join(pluginSkillsDir, entry.name, "SKILL.md"))
      .map((filePath) => parseFrontmatter(readFileSync(filePath, "utf-8")).frontmatter);

    const names = new Set(frontmatters.map((fm) => fm.name));

    // Every composesFrom slug must be the name of another skill in this pack.
    // Bare upstream slugs (e.g. "brainstorming", "systematic-debugging") are
    // invalid even if a contributor happens to have an upstream plugin
    // installed — the pack must be self-sufficient.
    const dangling = frontmatters.flatMap((fm) =>
      (Array.isArray(fm.composesFrom) ? fm.composesFrom : [])
        .filter((slug) => !names.has(slug))
        .map((slug) => `${fm.name} -> ${slug}`),
    );

    expect(dangling).toEqual([]);
  });

  it("parses scoped allowed-tools entries without splitting spaces inside scopes", () => {
    expect(parseAllowedTools("Bash(git worktree *) Bash(scripts/seed-worktree-mcp*) Grep")).toEqual([
      "Bash(git worktree *)",
      "Bash(scripts/seed-worktree-mcp*)",
      "Grep",
    ]);
  });

  it("includes the Build Studio decision and nonprod workflow skills", () => {
    const pluginSkillsDir = join(__dirname, "..", "..", "..", "packages", "dpf-skill-pack", "skills");
    const names = discoverDpfPlatformSkillFiles(pluginSkillsDir)
      .map((source) => parseFrontmatter(readFileSync(source.filePath, "utf8")).frontmatter.name)
      .sort();

    expect(names).toEqual(expect.arrayContaining([
      "dpf-capture-kernel-gap",
      "dpf-compare-options",
      "dpf-external-evidence-handoff",
      "dpf-local-merge-ci-before-push",
      "dpf-record-decision-outcome",
      "dpf-retrieve-decision-context",
      "dpf-use-shared-nonprod-environment",
    ]));
  });
});
