/**
 * Tests for Hermes dual-source skill tracking.
 * Verifies syncHermesNativeSkills and listAgentSkills typed return shape.
 *
 * Uses real temp directories instead of mocking readdir (ESM limitation prevents
 * direct spying on node:fs/promises named exports in Vitest).
 *
 * Covers: HERM-01, HERM-02, HERM-03
 */
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtemp, rm, mkdir } from "node:fs/promises";
import path from "node:path";
import os from "node:os";
import { getSkillRegistryDb, resetSkillRegistryDb } from "../services/skill-registry-db.js";
import { skills, agentSkills } from "../services/skill-registry-schema.js";
import { skillRegistryService } from "../services/skill-registry.js";
import { skillGroupService } from "../services/skill-registry-groups.js";
import { eq } from "drizzle-orm";

// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------

let tmpHome: string;
let fakeHermesSkillsDir: string;
let originalHome: string | undefined;

beforeEach(async () => {
  tmpHome = await mkdtemp(path.join(os.tmpdir(), "nexus-hermes-test-"));
  process.env.PAPERCLIP_HOME = tmpHome;

  // Override HOME so syncHermesNativeSkills reads our fake ~/.hermes/skills/
  originalHome = process.env.HOME;
  process.env.HOME = tmpHome;

  fakeHermesSkillsDir = path.join(tmpHome, ".hermes", "skills");
  resetSkillRegistryDb();
});

afterEach(async () => {
  resetSkillRegistryDb();
  delete process.env.PAPERCLIP_HOME;
  if (originalHome !== undefined) {
    process.env.HOME = originalHome;
  } else {
    delete process.env.HOME;
  }
  await rm(tmpHome, { recursive: true, force: true });
});

/** Create fake skill directories inside ~/.hermes/skills/ */
async function createFakeHermesSkills(skillNames: string[]): Promise<void> {
  await mkdir(fakeHermesSkillsDir, { recursive: true });
  for (const name of skillNames) {
    await mkdir(path.join(fakeHermesSkillsDir, name), { recursive: true });
  }
}

// ---------------------------------------------------------------------------
// syncHermesNativeSkills
// ---------------------------------------------------------------------------

describe("syncHermesNativeSkills()", () => {
  it("Test 1: creates skills stub rows with sourceId 'hermes-native'", async () => {
    await createFakeHermesSkills(["code-reviewer", "test-writer"]);

    const svc = skillRegistryService();
    await svc.syncHermesNativeSkills("agent-hermes-1");

    const db = await getSkillRegistryDb();
    const skillRows = await db.select().from(skills)
      .where(eq(skills.sourceId, "hermes-native"));

    expect(skillRows.length).toBe(2);
    expect(skillRows.map((r) => r.id).sort()).toEqual([
      "hermes-native/code-reviewer",
      "hermes-native/test-writer",
    ].sort());
    expect(skillRows.every((r) => r.sourceId === "hermes-native")).toBe(true);
  });

  it("Test 2: creates agentSkills rows with source 'native'", async () => {
    await createFakeHermesSkills(["code-reviewer"]);

    const svc = skillRegistryService();
    await svc.syncHermesNativeSkills("agent-hermes-2");

    const db = await getSkillRegistryDb();
    const agentSkillRows = await db.select().from(agentSkills)
      .where(eq(agentSkills.agentId, "agent-hermes-2"));

    expect(agentSkillRows.length).toBe(1);
    expect(agentSkillRows[0]!.skillId).toBe("hermes-native/code-reviewer");
    expect(agentSkillRows[0]!.source).toBe("native");
  });

  it("Test 3: is idempotent — running twice does not duplicate rows", async () => {
    await createFakeHermesSkills(["code-reviewer"]);

    const svc = skillRegistryService();
    // Run twice
    await svc.syncHermesNativeSkills("agent-hermes-3");
    await svc.syncHermesNativeSkills("agent-hermes-3");

    const db = await getSkillRegistryDb();
    const agentSkillRows = await db.select().from(agentSkills)
      .where(eq(agentSkills.agentId, "agent-hermes-3"));

    // Should only have ONE row despite calling sync twice
    expect(agentSkillRows.length).toBe(1);
  });

  it("Test 4: handles missing ~/.hermes/skills/ gracefully (no throw)", async () => {
    // Do NOT create the hermes skills dir — simulate it not existing

    const svc = skillRegistryService();
    // Must not throw
    await expect(svc.syncHermesNativeSkills("agent-hermes-4")).resolves.toBeUndefined();

    const db = await getSkillRegistryDb();
    const agentSkillRows = await db.select().from(agentSkills)
      .where(eq(agentSkills.agentId, "agent-hermes-4"));
    expect(agentSkillRows.length).toBe(0);
  });
});

// ---------------------------------------------------------------------------
// listAgentSkills — typed object shape
// ---------------------------------------------------------------------------

describe("listAgentSkills() — returns objects with source field", () => {
  it("Test 5: returns Array<{skillId, source, installedAt}> not string[]", async () => {
    const db = await getSkillRegistryDb();
    const agentId = "agent-shape-test";
    const now = Date.now();

    // Insert a managed skill directly
    await db.insert(agentSkills).values({
      agentId,
      skillId: "nexus-skills/code-review",
      installedAt: now,
      source: "managed",
    });

    const grpSvc = skillGroupService();
    const result = await grpSvc.listAgentSkills(agentId);

    expect(Array.isArray(result)).toBe(true);
    expect(result.length).toBe(1);

    const entry = result[0]!;
    // Must have the 3-field object shape
    expect(typeof entry.skillId).toBe("string");
    expect(typeof entry.source).toBe("string");
    expect(typeof entry.installedAt).toBe("number");

    // Must NOT be a plain string (old API)
    expect(typeof entry).toBe("object");
  });

  it("Test 6: returns skills with correct source values", async () => {
    const db = await getSkillRegistryDb();
    const agentId = "agent-source-values";
    const now = Date.now();

    await db.insert(agentSkills).values([
      { agentId, skillId: "nexus-skills/managed-skill", installedAt: now, source: "managed" },
      { agentId, skillId: "hermes-native/native-skill", installedAt: now, source: "native" },
    ]);

    const grpSvc = skillGroupService();
    const result = await grpSvc.listAgentSkills(agentId);

    expect(result.length).toBe(2);
    const managedEntry = result.find((r) => r.skillId === "nexus-skills/managed-skill");
    const nativeEntry = result.find((r) => r.skillId === "hermes-native/native-skill");

    expect(managedEntry?.source).toBe("managed");
    expect(nativeEntry?.source).toBe("native");
  });

  it("Test 7: listAgentSkills includes both managed and native skills for a Hermes agent", async () => {
    await createFakeHermesSkills(["native-skill"]);

    const agentId = "agent-hermes-combined";
    const db = await getSkillRegistryDb();
    const now = Date.now();

    // Insert a managed skill
    await db.insert(agentSkills).values({
      agentId,
      skillId: "nexus-skills/managed",
      installedAt: now,
      source: "managed",
    });

    // Sync native skills
    const svc = skillRegistryService();
    await svc.syncHermesNativeSkills(agentId);

    const grpSvc = skillGroupService();
    const result = await grpSvc.listAgentSkills(agentId);

    expect(result.length).toBe(2);

    const managed = result.filter((r) => r.source === "managed");
    const native = result.filter((r) => r.source === "native");

    expect(managed.length).toBe(1);
    expect(native.length).toBe(1);
    expect(managed[0]!.skillId).toBe("nexus-skills/managed");
    expect(native[0]!.skillId).toBe("hermes-native/native-skill");
  });
});
