import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
import { existsSync, readdirSync } 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 } from "../services/skill-registry-schema.js";
import { skillRegistryService } from "../services/skill-registry.js";

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

let tmpHome: string;
let tmpAgentSkillsDir: string;

beforeEach(async () => {
  // Create isolated temp dirs
  tmpHome = await mkdtemp(path.join(os.tmpdir(), "nexus-skill-registry-test-"));
  tmpAgentSkillsDir = path.join(tmpHome, "agent-skills");
  await mkdir(tmpAgentSkillsDir, { recursive: true });

  // Point PAPERCLIP_HOME at temp dir for DB and cache
  process.env.PAPERCLIP_HOME = tmpHome;
  resetSkillRegistryDb();
});

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

// Seed helpers

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: "A test skill",
    sourceUrl: `https://github.com/test/${opts.skillId}`,
    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): Promise<void> {
  await mkdir(cacheDir, { recursive: true });
  await writeFile(path.join(cacheDir, "SKILL.md"), "# Test Skill\n\nContent.", "utf-8");
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("skillRegistryService", () => {
  const svc = skillRegistryService();

  describe("install — SKILL.md-based skill", () => {
    it("Test 1: copies files from cache dir to agentSkillsDir/<slug>/", async () => {
      const skillId = "schwepps-skills/code-review";
      const versionId = `${skillId}@abc123`;
      const cacheDir = path.join(tmpHome, "skills", "cache", skillId, "abc123");
      await createFakeCacheDir(cacheDir);
      await seedSkillWithVersion({ skillId, sourceId: "schwepps-skills", versionId, cacheDir });

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

      expect(result.type).toBe("installed");
      if (result.type === "installed") {
        const slug = "code-review";
        const expectedTarget = path.join(tmpAgentSkillsDir, slug);
        expect(result.targetDir).toBe(expectedTarget);
        expect(existsSync(path.join(expectedTarget, "SKILL.md"))).toBe(true);
      }
    });

    it("Test 2: updates skill active_version_id to the latest version", async () => {
      const skillId = "schwepps-skills/code-review";
      const versionId = `${skillId}@abc123`;
      const cacheDir = path.join(tmpHome, "skills", "cache", skillId, "abc123");
      await createFakeCacheDir(cacheDir);
      await seedSkillWithVersion({ skillId, sourceId: "schwepps-skills", versionId, cacheDir });

      await svc.install(skillId, tmpAgentSkillsDir);

      const db = await getSkillRegistryDb();
      const rows = await db.select().from(skills).where(
        (await import("drizzle-orm")).eq(skills.id, skillId)
      );
      expect(rows[0]?.activeVersionId).toBe(versionId);
    });
  });

  describe("install — marketplace plugin", () => {
    it("Test 3: returns pending_plugin_install command instead of copying files for plugin kind", async () => {
      const skillId = "anthropic-official/my-plugin";
      const versionId = `${skillId}@deadbeef`;
      const cacheDir = path.join(tmpHome, "skills", "cache", skillId, "deadbeef");
      await createFakeCacheDir(cacheDir);
      await seedSkillWithVersion({
        skillId,
        sourceId: "anthropic-official",
        versionId,
        cacheDir,
        fileKind: "plugin",
      });

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

      expect(result.type).toBe("pending_plugin_install");
      if (result.type === "pending_plugin_install") {
        expect(result.command).toContain("/plugin install");
        expect(result.skillId).toBe(skillId);
        expect(result.versionId).toBe(versionId);
      }
      // No files should be copied to agent dir
      const agentFiles = readdirSync(tmpAgentSkillsDir);
      expect(agentFiles).toHaveLength(0);
    });
  });

  describe("uninstall", () => {
    it("Test 4: sets removed_at timestamp on skills row", async () => {
      const skillId = "schwepps-skills/code-review";
      const versionId = `${skillId}@abc123`;
      const cacheDir = path.join(tmpHome, "skills", "cache", skillId, "abc123");
      await createFakeCacheDir(cacheDir);
      await seedSkillWithVersion({ skillId, sourceId: "schwepps-skills", versionId, cacheDir });

      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: row still exists and is returned with includeRemoved=true", async () => {
      const skillId = "schwepps-skills/code-review";
      const versionId = `${skillId}@abc123`;
      const cacheDir = path.join(tmpHome, "skills", "cache", skillId, "abc123");
      await createFakeCacheDir(cacheDir);
      await seedSkillWithVersion({ skillId, sourceId: "schwepps-skills", versionId, cacheDir });

      await svc.uninstall(skillId, tmpAgentSkillsDir);

      // Not visible in normal list
      const normalList = await svc.list();
      expect(normalList.find((s) => s.id === skillId)).toBeUndefined();

      // Visible with includeRemoved
      const fullList = await svc.list({ includeRemoved: true });
      expect(fullList.find((s) => s.id === skillId)).toBeDefined();
    });
  });

  describe("rollback", () => {
    it("Test 6: copies prior version's cached files to agent skills dir", async () => {
      const skillId = "schwepps-skills/code-review";
      const slug = "code-review";

      // Seed v1 (prior) and v2 (current)
      const v1Id = `${skillId}@v1sha`;
      const v2Id = `${skillId}@v2sha`;

      const v1CacheDir = path.join(tmpHome, "skills", "cache", skillId, "v1sha");
      const v2CacheDir = path.join(tmpHome, "skills", "cache", skillId, "v2sha");

      await createFakeCacheDir(v1CacheDir);
      await writeFile(path.join(v1CacheDir, "SKILL.md"), "# Version 1", "utf-8");
      await createFakeCacheDir(v2CacheDir);
      await writeFile(path.join(v2CacheDir, "SKILL.md"), "# Version 2", "utf-8");

      // Seed both versions
      const db = await getSkillRegistryDb();
      const now = Date.now();
      await db.insert(skills).values({
        id: skillId,
        sourceId: "schwepps-skills",
        name: "Test",
        description: null,
        sourceUrl: "https://github.com/test",
        activeVersionId: v2Id,
        removedAt: null,
        createdAt: now,
        updatedAt: now,
      });
      await db.insert(skillVersions).values({
        id: v1Id, skillId, version: "v1sha", fetchedAt: now - 1000, cacheDir: v1CacheDir,
      });
      await db.insert(skillVersions).values({
        id: v2Id, skillId, version: "v2sha", fetchedAt: now, cacheDir: v2CacheDir,
      });
      await db.insert(skillFiles).values({
        id: "file-v1", versionId: v1Id, path: "SKILL.md", kind: "skill", sizeBytes: 12,
      });
      await db.insert(skillFiles).values({
        id: "file-v2", versionId: v2Id, path: "SKILL.md", kind: "skill", sizeBytes: 12,
      });

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

      // Rollback to v1
      await svc.rollback(skillId, v1Id, tmpAgentSkillsDir);

      // Verify v1 content is in place
      const { readFileSync } = await import("node:fs");
      const content = readFileSync(path.join(targetDir, "SKILL.md"), "utf-8");
      expect(content).toBe("# Version 1");
    });

    it("Test 7: updates active_version_id to the prior version", async () => {
      const skillId = "schwepps-skills/code-review";
      const v1Id = `${skillId}@v1sha`;
      const v2Id = `${skillId}@v2sha`;

      const v1CacheDir = path.join(tmpHome, "skills", "cache", skillId, "v1sha");
      await createFakeCacheDir(v1CacheDir);

      const db = await getSkillRegistryDb();
      const now = Date.now();
      await db.insert(skills).values({
        id: skillId, sourceId: "test", name: "T", description: null,
        sourceUrl: "u", activeVersionId: v2Id, removedAt: null, createdAt: now, updatedAt: now,
      });
      await db.insert(skillVersions).values({
        id: v1Id, skillId, version: "v1sha", fetchedAt: now - 1000, cacheDir: v1CacheDir,
      });

      const agentDir = path.join(tmpAgentSkillsDir, "code-review");
      await mkdir(agentDir, { recursive: true });
      await writeFile(path.join(agentDir, "SKILL.md"), "current", "utf-8");

      await svc.rollback(skillId, v1Id, tmpAgentSkillsDir);

      const { eq } = await import("drizzle-orm");
      const rows = await db.select().from(skills).where(eq(skills.id, skillId));
      expect(rows[0]?.activeVersionId).toBe(v1Id);
    });
  });

  describe("list", () => {
    it("Test 8: returns only skills where removed_at IS NULL by default", async () => {
      const db = await getSkillRegistryDb();
      const now = Date.now();
      await db.insert(skills).values([
        {
          id: "active-skill", sourceId: "test", name: "Active", description: null,
          sourceUrl: "u", activeVersionId: null, removedAt: null, createdAt: now, updatedAt: now,
        },
        {
          id: "removed-skill", sourceId: "test", name: "Removed", description: null,
          sourceUrl: "u", activeVersionId: null, removedAt: now - 1000, createdAt: now, updatedAt: now,
        },
      ]);

      const result = await svc.list();
      const ids = result.map((s) => s.id);

      expect(ids).toContain("active-skill");
      expect(ids).not.toContain("removed-skill");
    });

    it("Test 9: list({ includeRemoved: true }) returns all skills", async () => {
      const db = await getSkillRegistryDb();
      const now = Date.now();
      await db.insert(skills).values([
        {
          id: "active-skill", sourceId: "test", name: "Active", description: null,
          sourceUrl: "u", activeVersionId: null, removedAt: null, createdAt: now, updatedAt: now,
        },
        {
          id: "removed-skill", sourceId: "test", name: "Removed", description: null,
          sourceUrl: "u", activeVersionId: null, removedAt: now - 1000, createdAt: now, updatedAt: now,
        },
      ]);

      const result = await svc.list({ includeRemoved: true });
      const ids = result.map((s) => s.id);

      expect(ids).toContain("active-skill");
      expect(ids).toContain("removed-skill");
    });
  });
});
