import { randomUUID } from "node:crypto";
import os from "node:os";
import path from "node:path";
import { promises as fs } from "node:fs";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { agents, companies, companySkills, createDb } from "@paperclipai/db";
import {
  getEmbeddedPostgresTestSupport,
  startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
import { companySkillService } from "../services/company-skills.ts";

const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;

if (!embeddedPostgresSupport.supported) {
  console.warn(
    `Skipping embedded Postgres company skill service tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
  );
}

describeEmbeddedPostgres("companySkillService.list", () => {
  let db!: ReturnType<typeof createDb>;
  let svc!: ReturnType<typeof companySkillService>;
  let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
  let oldPaperclipHome: string | undefined;
  let paperclipHome: string | null = null;
  const cleanupDirs = new Set<string>();

  beforeAll(async () => {
    tempDb = await startEmbeddedPostgresTestDatabase("paperclip-company-skills-service-");
    oldPaperclipHome = process.env.PAPERCLIP_HOME;
    paperclipHome = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-company-skills-home-"));
    process.env.PAPERCLIP_HOME = paperclipHome;
    db = createDb(tempDb.connectionString);
    svc = companySkillService(db);
  }, 20_000);

  afterEach(async () => {
    await db.delete(agents);
    await db.delete(companySkills);
    await db.delete(companies);
    await Promise.all(Array.from(cleanupDirs, (dir) => fs.rm(dir, { recursive: true, force: true })));
    cleanupDirs.clear();
  });

  afterAll(async () => {
    if (oldPaperclipHome === undefined) delete process.env.PAPERCLIP_HOME;
    else process.env.PAPERCLIP_HOME = oldPaperclipHome;
    if (paperclipHome) {
      await fs.rm(paperclipHome, { recursive: true, force: true });
    }
    await tempDb?.cleanup();
  });

  it("lists skills without exposing markdown content", async () => {
    const companyId = randomUUID();
    const skillId = randomUUID();
    const skillDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-heavy-skill-"));
    cleanupDirs.add(skillDir);
    await fs.writeFile(path.join(skillDir, "SKILL.md"), "# Heavy Skill\n", "utf8");

    await db.insert(companies).values({
      id: companyId,
      name: "Paperclip",
      issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
      requireBoardApprovalForNewAgents: false,
    });

    await db.insert(companySkills).values({
      id: skillId,
      companyId,
      key: `company/${companyId}/heavy-skill`,
      slug: "heavy-skill",
      name: "Heavy Skill",
      description: "Large skill used for list projection regression coverage.",
      markdown: `# Heavy Skill\n\n${"x".repeat(250_000)}`,
      sourceType: "local_path",
      sourceLocator: skillDir,
      trustLevel: "markdown_only",
      compatibility: "compatible",
      fileInventory: [{ path: "SKILL.md", kind: "skill" }],
      metadata: { sourceKind: "local_path" },
    });

    const listed = await svc.list(companyId);
    const skill = listed.find((entry) => entry.id === skillId);

    expect(skill).toBeDefined();
    expect(skill).not.toHaveProperty("markdown");
    expect(skill).toMatchObject({
      id: skillId,
      key: `company/${companyId}/heavy-skill`,
      slug: "heavy-skill",
      name: "Heavy Skill",
      sourceType: "local_path",
      sourceLocator: skillDir,
      attachedAgentCount: 0,
      sourceBadge: "local",
      editable: true,
    });
  });

  it("rejects skill inventory refresh for a missing company", async () => {
    await expect(svc.list(randomUUID())).rejects.toMatchObject({
      status: 404,
      message: "Company not found",
    });
  });

  it("does not persist audit failures for remote-source skills", async () => {
    const companyId = randomUUID();
    const skillId = randomUUID();
    await db.insert(companies).values({
      id: companyId,
      name: "Paperclip",
      issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
      requireBoardApprovalForNewAgents: false,
    });
    await db.insert(companySkills).values({
      id: skillId,
      companyId,
      key: "github.com/acme/remote-skill",
      slug: "remote-skill",
      name: "Remote Skill",
      description: null,
      markdown: "# Remote Skill\n",
      sourceType: "github",
      sourceLocator: "https://github.com/acme/remote-skill",
      sourceRef: "main",
      trustLevel: "markdown_only",
      compatibility: "compatible",
      fileInventory: [{ path: "SKILL.md", kind: "skill" }],
      metadata: { sourceKind: "github", owner: "acme", repo: "remote-skill" },
    });

    await expect(svc.auditSkill(companyId, skillId)).rejects.toMatchObject({
      status: 422,
      message: "Only local-path and catalog-managed company skills support audit.",
    });
    await expect(svc.getById(companyId, skillId)).resolves.toMatchObject({
      metadata: { sourceKind: "github", owner: "acme", repo: "remote-skill" },
    });
  });

  it("preserves missing local-path skills that active agents still desire", async () => {
    const companyId = randomUUID();
    const skillId = randomUUID();
    const skillKey = `company/${companyId}/reflection-coach`;
    const missingSkillDir = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-missing-used-skill-")), "gone");
    cleanupDirs.add(path.dirname(missingSkillDir));

    await db.insert(companies).values({
      id: companyId,
      name: "Paperclip",
      issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
      requireBoardApprovalForNewAgents: false,
    });
    await db.insert(companySkills).values({
      id: skillId,
      companyId,
      key: skillKey,
      slug: "reflection-coach",
      name: "Reflection Coach",
      description: null,
      markdown: "# Reflection Coach\n",
      sourceType: "local_path",
      sourceLocator: missingSkillDir,
      trustLevel: "markdown_only",
      compatibility: "compatible",
      fileInventory: [{ path: "SKILL.md", kind: "skill" }],
      metadata: { sourceKind: "local_path" },
    });
    await db.insert(agents).values({
      id: randomUUID(),
      companyId,
      name: "Reviewer",
      role: "engineer",
      status: "active",
      adapterType: "codex_local",
      adapterConfig: {
        paperclipSkillSync: {
          desiredSkills: [skillKey],
        },
      },
    });

    const listed = await svc.list(companyId);
    const listedSkill = listed.find((skill) => skill.id === skillId);
    const detail = await svc.detail(companyId, skillId);
    const stored = await svc.getById(companyId, skillId);
    const marker = stored?.metadata?.missingSource;

    expect(listedSkill).toMatchObject({
      id: skillId,
      attachedAgentCount: 1,
    });
    expect(detail?.usedByAgents).toEqual([
      expect.objectContaining({
        name: "Reviewer",
        desired: true,
      }),
    ]);
    expect(marker).toMatchObject({
      reason: "local_source_missing",
      sourceType: "local_path",
      sourceLocator: missingSkillDir,
      sourcePath: missingSkillDir,
    });
    expect(Number.isNaN(Date.parse(String((marker as Record<string, unknown>).detectedAt)))).toBe(false);
  });

  it("continues pruning missing local-path skills that no active agent desires", async () => {
    const companyId = randomUUID();
    const skillId = randomUUID();
    const missingSkillDir = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-missing-unused-skill-")), "gone");
    cleanupDirs.add(path.dirname(missingSkillDir));

    await db.insert(companies).values({
      id: companyId,
      name: "Paperclip",
      issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
      requireBoardApprovalForNewAgents: false,
    });
    await db.insert(companySkills).values({
      id: skillId,
      companyId,
      key: `company/${companyId}/unused-skill`,
      slug: "unused-skill",
      name: "Unused Skill",
      description: null,
      markdown: "# Unused Skill\n",
      sourceType: "local_path",
      sourceLocator: missingSkillDir,
      trustLevel: "markdown_only",
      compatibility: "compatible",
      fileInventory: [{ path: "SKILL.md", kind: "skill" }],
      metadata: { sourceKind: "local_path" },
    });

    const listed = await svc.list(companyId);

    expect(listed.find((skill) => skill.id === skillId)).toBeUndefined();
    await expect(svc.getById(companyId, skillId)).resolves.toBeNull();
  });

  it("clears the missing-source marker when a local-path skill source returns", async () => {
    const companyId = randomUUID();
    const skillId = randomUUID();
    const skillDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-restored-skill-"));
    cleanupDirs.add(skillDir);
    await fs.writeFile(path.join(skillDir, "SKILL.md"), "# Restored Skill\n", "utf8");

    await db.insert(companies).values({
      id: companyId,
      name: "Paperclip",
      issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
      requireBoardApprovalForNewAgents: false,
    });
    await db.insert(companySkills).values({
      id: skillId,
      companyId,
      key: `company/${companyId}/restored-skill`,
      slug: "restored-skill",
      name: "Restored Skill",
      description: null,
      markdown: "# Restored Skill\n",
      sourceType: "local_path",
      sourceLocator: skillDir,
      trustLevel: "markdown_only",
      compatibility: "compatible",
      fileInventory: [{ path: "SKILL.md", kind: "skill" }],
      metadata: {
        sourceKind: "local_path",
        missingSource: {
          reason: "local_source_missing",
          sourceType: "local_path",
          sourceLocator: skillDir,
          sourcePath: skillDir,
          detectedAt: "2026-05-28T00:00:00.000Z",
        },
      },
    });

    await svc.list(companyId);
    const stored = await svc.getById(companyId, skillId);

    expect(stored?.metadata).toEqual({ sourceKind: "local_path" });
  });

  it("marks source-missing company skills as unavailable during read-only runtime listing", async () => {
    const companyId = randomUUID();
    const skillId = randomUUID();
    const skillKey = `company/${companyId}/reflection-coach`;
    const missingSkillDir = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-readonly-missing-skill-")), "gone");
    cleanupDirs.add(path.dirname(missingSkillDir));

    await db.insert(companies).values({
      id: companyId,
      name: "Paperclip",
      issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
      requireBoardApprovalForNewAgents: false,
    });
    await db.insert(companySkills).values({
      id: skillId,
      companyId,
      key: skillKey,
      slug: "reflection-coach",
      name: "Reflection Coach",
      description: null,
      markdown: "# Reflection Coach\n",
      sourceType: "local_path",
      sourceLocator: missingSkillDir,
      trustLevel: "markdown_only",
      compatibility: "compatible",
      fileInventory: [{ path: "SKILL.md", kind: "skill" }],
      metadata: { sourceKind: "local_path" },
    });
    await db.insert(agents).values({
      id: randomUUID(),
      companyId,
      name: "Reviewer",
      role: "engineer",
      status: "active",
      adapterType: "codex_local",
      adapterConfig: {
        paperclipSkillSync: {
          desiredSkills: [skillKey],
        },
      },
    });

    const entries = await svc.listRuntimeSkillEntries(companyId, { materializeMissing: false });
    const entry = entries.find((candidate) => candidate.key === skillKey);

    expect(entry).toMatchObject({
      key: skillKey,
      sourceStatus: "missing",
      missingDetail: expect.stringContaining(missingSkillDir),
    });
    await expect(fs.stat(entry!.source)).rejects.toMatchObject({ code: "ENOENT" });
  });

  it("materializes source-missing company skills from the stored markdown during runtime listing", async () => {
    const companyId = randomUUID();
    const skillId = randomUUID();
    const skillKey = `company/${companyId}/runtime-coach`;
    const missingSkillDir = path.join(await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-missing-skill-")), "gone");
    cleanupDirs.add(path.dirname(missingSkillDir));

    await db.insert(companies).values({
      id: companyId,
      name: "Paperclip",
      issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
      requireBoardApprovalForNewAgents: false,
    });
    await db.insert(companySkills).values({
      id: skillId,
      companyId,
      key: skillKey,
      slug: "runtime-coach",
      name: "Runtime Coach",
      description: null,
      markdown: "# Runtime Coach\n\nRecovered from DB.\n",
      sourceType: "local_path",
      sourceLocator: missingSkillDir,
      trustLevel: "markdown_only",
      compatibility: "compatible",
      fileInventory: [{ path: "SKILL.md", kind: "skill" }],
      metadata: { sourceKind: "local_path" },
    });
    await db.insert(agents).values({
      id: randomUUID(),
      companyId,
      name: "Runner",
      role: "engineer",
      status: "active",
      adapterType: "codex_local",
      adapterConfig: {
        paperclipSkillSync: {
          desiredSkills: [skillKey],
        },
      },
    });

    const entries = await svc.listRuntimeSkillEntries(companyId);
    const entry = entries.find((candidate) => candidate.key === skillKey);

    expect(entry).toMatchObject({
      key: skillKey,
      sourceStatus: "available",
    });
    await expect(fs.readFile(path.join(entry!.source, "SKILL.md"), "utf8")).resolves.toBe(
      "# Runtime Coach\n\nRecovered from DB.\n",
    );
  });
});
