/**
 * Tests for adapter-aware install/uninstall/rollback/assignGroup/removeGroup.
 * Verifies that all file operations use the caller-supplied agentSkillsDir
 * rather than any hardcoded path.
 *
 * Covers: INST-01, INST-02, INST-03, INST-04
 */
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtemp, rm, mkdir, writeFile, readdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
import os from "node:os";
import { getSkillRegistryDb, resetSkillRegistryDb } from "../services/skill-registry-db.js";
import { skills, skillVersions, skillFiles, agentSkills } from "../services/skill-registry-schema.js";
import { skillRegistryService } from "../services/skill-registry.js";
import { skillGroupService } from "../services/skill-registry-groups.js";

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

let tmpHome: string;
let tmpAgentSkillsDir: string;

beforeEach(async () => {
  tmpHome = await mkdtemp(path.join(os.tmpdir(), "nexus-adapter-install-test-"));
  tmpAgentSkillsDir = path.join(tmpHome, "agent-skills");
  await mkdir(tmpAgentSkillsDir, { recursive: true });
  process.env.PAPERCLIP_HOME = tmpHome;
  resetSkillRegistryDb();
});

afterEach(async () => {
  resetSkillRegistryDb();
  delete process.env.PAPERCLIP_HOME;
  await rm(tmpHome, { recursive: true, force: true });
});

async function seedSkillWithVersion(opts: {
  skillId: string;
  sourceId: string;
  versionId: string;
  cacheDir?: string;
  fileKind?: string;
}): Promise<void> {
  const db = await getSkillRegistryDb();
  const now = Date.now();
  await db.insert(skills).values({
    id: opts.skillId,
    sourceId: opts.sourceId,
    name: "Test Skill",
    description: null,
    sourceUrl: null,
    activeVersionId: null,
    removedAt: null,
    createdAt: now,
    updatedAt: now,
  });
  await db.insert(skillVersions).values({
    id: opts.versionId,
    skillId: opts.skillId,
    version: "abc123",
    fetchedAt: now,
    cacheDir: opts.cacheDir ?? null,
  });
  await db.insert(skillFiles).values({
    id: `file-${opts.versionId}`,
    versionId: opts.versionId,
    path: "SKILL.md",
    kind: opts.fileKind ?? "skill",
    sizeBytes: 100,
  });
}

async function createFakeCacheDir(cacheDir: string, content = "# Test Skill"): Promise<void> {
  await mkdir(cacheDir, { recursive: true });
  await writeFile(path.join(cacheDir, "SKILL.md"), content, "utf-8");
}

// ---------------------------------------------------------------------------
// install() — writes to provided agentSkillsDir
// ---------------------------------------------------------------------------

describe("install() — adapter-aware path", () => {
  it("Test 1: writes skill files to the provided agentSkillsDir, not a hardcoded path", async () => {
    const skillId = "nexus-skills/typescript-review";
    const versionId = `${skillId}@abc123`;
    const cacheDir = path.join(tmpHome, "cache", "typescript-review");
    await createFakeCacheDir(cacheDir);
    await seedSkillWithVersion({ skillId, sourceId: "nexus-skills", versionId, cacheDir });

    const svc = skillRegistryService();
    const result = await svc.install(skillId, tmpAgentSkillsDir);

    expect(result.type).toBe("installed");
    if (result.type === "installed") {
      // Files must land inside tmpAgentSkillsDir, not any hardcoded path
      expect(result.targetDir).toContain(tmpAgentSkillsDir);
      expect(existsSync(path.join(result.targetDir, "SKILL.md"))).toBe(true);
    }
  });

  it("Test 2: slug (last segment of skillId) is used as subdirectory name", async () => {
    const skillId = "nexus-skills/code-review";
    const versionId = `${skillId}@sha1`;
    const cacheDir = path.join(tmpHome, "cache", "code-review");
    await createFakeCacheDir(cacheDir);
    await seedSkillWithVersion({ skillId, sourceId: "nexus-skills", versionId, cacheDir });

    const svc = skillRegistryService();
    const result = await svc.install(skillId, tmpAgentSkillsDir);

    expect(result.type).toBe("installed");
    if (result.type === "installed") {
      expect(result.targetDir).toBe(path.join(tmpAgentSkillsDir, "code-review"));
    }
  });
});

// ---------------------------------------------------------------------------
// uninstall() — removes files AND soft-deletes DB row
// ---------------------------------------------------------------------------

describe("uninstall() — file removal + soft-delete", () => {
  it("Test 3: removes skill directory from disk", async () => {
    const skillId = "nexus-skills/code-review";
    const versionId = `${skillId}@abc123`;
    const cacheDir = path.join(tmpHome, "cache", "code-review");
    await createFakeCacheDir(cacheDir);
    await seedSkillWithVersion({ skillId, sourceId: "nexus-skills", versionId, cacheDir });

    const svc = skillRegistryService();
    // First install so files are on disk
    await svc.install(skillId, tmpAgentSkillsDir);
    const targetDir = path.join(tmpAgentSkillsDir, "code-review");
    expect(existsSync(targetDir)).toBe(true);

    // Now uninstall — should remove files
    await svc.uninstall(skillId, tmpAgentSkillsDir);
    expect(existsSync(targetDir)).toBe(false);
  });

  it("Test 4: soft-deletes the DB row (sets removedAt)", async () => {
    const skillId = "nexus-skills/code-review";
    const versionId = `${skillId}@abc123`;
    const cacheDir = path.join(tmpHome, "cache", "code-review");
    await createFakeCacheDir(cacheDir);
    await seedSkillWithVersion({ skillId, sourceId: "nexus-skills", versionId, cacheDir });

    const svc = skillRegistryService();
    const before = Date.now();
    await svc.uninstall(skillId, tmpAgentSkillsDir);
    const after = Date.now();

    const db = await getSkillRegistryDb();
    const { eq } = await import("drizzle-orm");
    const rows = await db.select().from(skills).where(eq(skills.id, skillId));
    expect(rows[0]?.removedAt).toBeGreaterThanOrEqual(before);
    expect(rows[0]?.removedAt).toBeLessThanOrEqual(after);
  });

  it("Test 5: uninstall with non-existent directory does not throw (force: true)", async () => {
    const skillId = "nexus-skills/missing-skill";
    await seedSkillWithVersion({
      skillId,
      sourceId: "nexus-skills",
      versionId: `${skillId}@sha1`,
    });

    const svc = skillRegistryService();
    // targetDir never existed — should not throw
    await expect(svc.uninstall(skillId, tmpAgentSkillsDir)).resolves.toBeUndefined();
  });

  it("Test 6: uninstall removes files from the correct path (slug-based subdirectory)", async () => {
    const skillId = "my-source/my-skill-name";
    const versionId = `${skillId}@abc123`;
    const cacheDir = path.join(tmpHome, "cache", "my-skill-name");
    await createFakeCacheDir(cacheDir);
    await seedSkillWithVersion({ skillId, sourceId: "my-source", versionId, cacheDir });

    const svc = skillRegistryService();
    await svc.install(skillId, tmpAgentSkillsDir);

    // Confirm the correct subdirectory was created
    const expectedDir = path.join(tmpAgentSkillsDir, "my-skill-name");
    expect(existsSync(expectedDir)).toBe(true);

    await svc.uninstall(skillId, tmpAgentSkillsDir);
    expect(existsSync(expectedDir)).toBe(false);
  });
});

// ---------------------------------------------------------------------------
// rollback() — restores files to provided agentSkillsDir
// ---------------------------------------------------------------------------

describe("rollback() — restores to provided agentSkillsDir", () => {
  it("Test 7: restores files to the provided agentSkillsDir (not a hardcoded path)", async () => {
    const skillId = "nexus-skills/code-review";
    const v1Id = `${skillId}@v1`;
    const v2Id = `${skillId}@v2`;
    const v1CacheDir = path.join(tmpHome, "cache", skillId, "v1");
    const v2CacheDir = path.join(tmpHome, "cache", skillId, "v2");

    await createFakeCacheDir(v1CacheDir, "# Version 1");
    await createFakeCacheDir(v2CacheDir, "# Version 2");

    const db = await getSkillRegistryDb();
    const now = Date.now();
    await db.insert(skills).values({
      id: skillId, sourceId: "nexus-skills", name: "Code Review",
      description: null, sourceUrl: null, activeVersionId: v2Id,
      removedAt: null, createdAt: now, updatedAt: now,
    });
    await db.insert(skillVersions).values({ id: v1Id, skillId, version: "v1", fetchedAt: now - 1000, cacheDir: v1CacheDir });
    await db.insert(skillVersions).values({ id: v2Id, skillId, version: "v2", fetchedAt: now, cacheDir: v2CacheDir });
    await db.insert(skillFiles).values({ id: "f1", versionId: v1Id, path: "SKILL.md", kind: "skill", sizeBytes: 12 });
    await db.insert(skillFiles).values({ id: "f2", versionId: v2Id, path: "SKILL.md", kind: "skill", sizeBytes: 12 });

    // Pre-install v2
    const targetDir = path.join(tmpAgentSkillsDir, "code-review");
    await mkdir(targetDir, { recursive: true });
    await writeFile(path.join(targetDir, "SKILL.md"), "# Version 2", "utf-8");

    // Rollback to v1 using provided agentSkillsDir
    const svc = skillRegistryService();
    await svc.rollback(skillId, v1Id, tmpAgentSkillsDir);

    // Files must be in tmpAgentSkillsDir, not any hardcoded path
    const { readFileSync } = await import("node:fs");
    const content = readFileSync(path.join(tmpAgentSkillsDir, "code-review", "SKILL.md"), "utf-8");
    expect(content).toBe("# Version 1");
  });
});

// ---------------------------------------------------------------------------
// assignGroup() / removeGroup() — use provided agentSkillsDir
// ---------------------------------------------------------------------------

describe("assignGroup() — uses provided agentSkillsDir", () => {
  it("Test 8: assignGroup installs skills into the provided agentSkillsDir", async () => {
    const skillId = "nexus-skills/code-review";
    const versionId = `${skillId}@abc123`;
    const cacheDir = path.join(tmpHome, "cache", "code-review");
    await createFakeCacheDir(cacheDir);
    await seedSkillWithVersion({ skillId, sourceId: "nexus-skills", versionId, cacheDir });

    const db = await getSkillRegistryDb();
    const now = Date.now();
    const groupId = "custom/test-group";
    await db.insert((await import("../services/skill-registry-schema.js")).skillGroups).values({
      id: groupId, name: "Test Group", description: null, isBuiltin: 0, createdAt: now, updatedAt: now,
    });
    await db.insert((await import("../services/skill-registry-schema.js")).skillGroupMembers).values({
      groupId, skillId, addedAt: now,
    });

    const grpSvc = skillGroupService();
    const result = await grpSvc.assignGroup(groupId, "agent-123", tmpAgentSkillsDir);

    // The skill should have been installed into our provided dir
    expect(result.installed).toContain(skillId);
    expect(existsSync(path.join(tmpAgentSkillsDir, "code-review", "SKILL.md"))).toBe(true);
  });
});

describe("removeGroup() — uses provided agentSkillsDir", () => {
  it("Test 9: removeGroup removes skill files from the provided agentSkillsDir", async () => {
    const skillId = "nexus-skills/code-review";
    const versionId = `${skillId}@abc123`;
    const cacheDir = path.join(tmpHome, "cache", "code-review");
    await createFakeCacheDir(cacheDir);
    await seedSkillWithVersion({ skillId, sourceId: "nexus-skills", versionId, cacheDir });

    const db = await getSkillRegistryDb();
    const now = Date.now();
    const groupId = "custom/test-group";
    const agentId = "agent-xyz";
    const { skillGroups: grps, skillGroupMembers: grpMembers } = await import("../services/skill-registry-schema.js");
    await db.insert(grps).values({
      id: groupId, name: "Test Group", description: null, isBuiltin: 0, createdAt: now, updatedAt: now,
    });
    await db.insert(grpMembers).values({ groupId, skillId, addedAt: now });

    const grpSvc = skillGroupService();
    // Assign group (installs skills)
    await grpSvc.assignGroup(groupId, agentId, tmpAgentSkillsDir);
    expect(existsSync(path.join(tmpAgentSkillsDir, "code-review"))).toBe(true);

    // Remove group — should remove files from provided dir
    await grpSvc.removeGroup(groupId, agentId, tmpAgentSkillsDir);
    expect(existsSync(path.join(tmpAgentSkillsDir, "code-review"))).toBe(false);
  });
});
