import { mkdtemp, rm, readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";

// ---------------------------------------------------------------------------
// Helpers for building mock responses
// ---------------------------------------------------------------------------

function mockMarketplaceJson(skills: Array<{ path: string }>) {
  return JSON.stringify({ skills });
}

function mockGitHubTree(paths: string[]) {
  return JSON.stringify({
    tree: paths.map((p) => ({ path: p, type: "blob", size: 100 })),
  });
}

function mockSkillMd(name: string, description: string) {
  return `---
name: ${name}
description: ${description}
---

# ${name}

A skill for testing.
`;
}

function mockCommitSha(sha = "abc1234567890abcdef1234567890abcdef123456") {
  return JSON.stringify({ sha });
}

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

describe("skill-registry-fetch", () => {
  let tmpDir: string;
  let originalPaperclipHome: string | undefined;
  let fetchSpy: ReturnType<typeof vi.fn>;

  beforeEach(async () => {
    tmpDir = await mkdtemp(path.join(os.tmpdir(), "skill-fetch-test-"));
    originalPaperclipHome = process.env.PAPERCLIP_HOME;
    process.env.PAPERCLIP_HOME = tmpDir;

    // Mock global.fetch
    fetchSpy = vi.fn();
    vi.stubGlobal("fetch", fetchSpy);
  });

  afterEach(async () => {
    vi.restoreAllMocks();
    vi.unstubAllGlobals();

    const { resetSkillRegistryDb } = await import("../services/skill-registry-db.js");
    resetSkillRegistryDb();

    if (originalPaperclipHome === undefined) {
      delete process.env.PAPERCLIP_HOME;
    } else {
      process.env.PAPERCLIP_HOME = originalPaperclipHome;
    }

    await rm(tmpDir, { recursive: true, force: true });
  });

  // -------------------------------------------------------------------------
  // Test 1: Anthropic marketplace source inserts skills rows with sourceId
  // -------------------------------------------------------------------------
  it("Test 1: fetchAllSources() with mocked Anthropic marketplace.json inserts skills rows with sourceId='anthropic-official'", async () => {
    const sha = "abc1234567890abcdef1234567890abcdef123456";

    // Mock all fetch calls
    fetchSpy.mockImplementation(async (url: string, opts?: RequestInit) => {
      const urlStr = String(url);

      // marketplace.json
      if (urlStr.includes("marketplace.json")) {
        return {
          ok: true,
          text: async () => mockMarketplaceJson([{ path: "coding/my-skill" }]),
          json: async () => ({ skills: [{ path: "coding/my-skill" }] }),
        };
      }
      // commit SHA lookup
      if (urlStr.includes("/commits/")) {
        return {
          ok: true,
          text: async () => mockCommitSha(sha),
          json: async () => ({ sha }),
        };
      }
      // SKILL.md raw content
      if (urlStr.includes("SKILL.md")) {
        return {
          ok: true,
          text: async () => mockSkillMd("My Skill", "A test skill from Anthropic"),
          json: async () => ({}),
        };
      }
      return { ok: false, status: 404, text: async () => "Not found", json: async () => ({}) };
    });

    const { fetchAllSources, BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js");
    const { getSkillRegistryDb } = await import("../services/skill-registry-db.js");
    const { skills } = await import("../services/skill-registry-schema.js");

    // Fetch only the Anthropic source
    const anthropicSource = BUILT_IN_SOURCES.find((s) => s.id === "anthropic-official")!;
    const result = await fetchAllSources([anthropicSource]);

    expect(result.errors).toHaveLength(0);
    expect(result.fetched).toBeGreaterThan(0);

    const db = await getSkillRegistryDb();
    const rows = await db.select().from(skills);
    expect(rows.length).toBeGreaterThan(0);
    expect(rows[0]!.sourceId).toBe("anthropic-official");
  });

  // -------------------------------------------------------------------------
  // Test 2: Community GitHub source inserts skills rows with correct sourceId
  // -------------------------------------------------------------------------
  it("Test 2: fetchAllSources() with mocked GitHub tree API for community repo inserts skills rows with correct sourceId", async () => {
    const sha = "deadbeef1234567890abcdef1234567890abcdef";

    fetchSpy.mockImplementation(async (url: string) => {
      const urlStr = String(url);

      if (urlStr.includes("/git/trees/")) {
        return {
          ok: true,
          text: async () => mockGitHubTree(["code-review/SKILL.md", "code-review/rules/rules.md"]),
          json: async () => ({
            tree: [
              { path: "code-review/SKILL.md", type: "blob", size: 200 },
              { path: "code-review/rules/rules.md", type: "blob", size: 500 },
            ],
          }),
        };
      }
      if (urlStr.includes("/commits/")) {
        return {
          ok: true,
          text: async () => mockCommitSha(sha),
          json: async () => ({ sha }),
        };
      }
      if (urlStr.includes("SKILL.md")) {
        return {
          ok: true,
          text: async () => mockSkillMd("Code Review", "Reviews code for quality"),
          json: async () => ({}),
        };
      }
      return { ok: false, status: 404, text: async () => "Not found", json: async () => ({}) };
    });

    const { fetchAllSources, BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js");
    const { getSkillRegistryDb } = await import("../services/skill-registry-db.js");
    const { skills } = await import("../services/skill-registry-schema.js");

    const communitySource = BUILT_IN_SOURCES.find((s) => s.id === "schwepps-skills")!;
    const result = await fetchAllSources([communitySource]);

    expect(result.errors).toHaveLength(0);

    const db = await getSkillRegistryDb();
    const rows = await db.select().from(skills);
    expect(rows.length).toBeGreaterThan(0);
    expect(rows[0]!.sourceId).toBe("schwepps-skills");
  });

  // -------------------------------------------------------------------------
  // Test 3: Each fetched skill has a skill_versions row with commit SHA
  // -------------------------------------------------------------------------
  it("Test 3: Each fetched skill has a skill_versions row with the commit SHA as version", async () => {
    const sha = "cafebabe1234567890abcdef1234567890abcdef";

    fetchSpy.mockImplementation(async (url: string) => {
      const urlStr = String(url);

      if (urlStr.includes("/git/trees/")) {
        return {
          ok: true,
          json: async () => ({
            tree: [{ path: "test-skill/SKILL.md", type: "blob", size: 100 }],
          }),
        };
      }
      if (urlStr.includes("/commits/")) {
        return {
          ok: true,
          json: async () => ({ sha }),
        };
      }
      if (urlStr.includes("SKILL.md")) {
        return {
          ok: true,
          text: async () => mockSkillMd("Test Skill", "A test skill"),
        };
      }
      return { ok: false, status: 404, text: async () => "Not found", json: async () => ({}) };
    });

    const { fetchAllSources, BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js");
    const { getSkillRegistryDb } = await import("../services/skill-registry-db.js");
    const { skillVersions } = await import("../services/skill-registry-schema.js");

    const source = BUILT_IN_SOURCES.find((s) => s.id === "schwepps-skills")!;
    await fetchAllSources([source]);

    const db = await getSkillRegistryDb();
    const versions = await db.select().from(skillVersions);
    expect(versions.length).toBeGreaterThan(0);
    expect(versions[0]!.version).toBe(sha);
  });

  // -------------------------------------------------------------------------
  // Test 4: SKILL.md written to cache dir
  // -------------------------------------------------------------------------
  it("Test 4: Each fetched skill's SKILL.md content is written to cache dir at <instance-root>/skills/cache/<skill-id>/<sha>/SKILL.md", async () => {
    const sha = "feedfeed1234567890abcdef1234567890abcdef";
    const skillMdContent = mockSkillMd("Cached Skill", "Written to disk");

    fetchSpy.mockImplementation(async (url: string) => {
      const urlStr = String(url);

      if (urlStr.includes("/git/trees/")) {
        return {
          ok: true,
          json: async () => ({
            tree: [{ path: "cached-skill/SKILL.md", type: "blob", size: 100 }],
          }),
        };
      }
      if (urlStr.includes("/commits/")) {
        return {
          ok: true,
          json: async () => ({ sha }),
        };
      }
      if (urlStr.includes("SKILL.md")) {
        return {
          ok: true,
          text: async () => skillMdContent,
        };
      }
      return { ok: false, status: 404, text: async () => "Not found", json: async () => ({}) };
    });

    const { fetchAllSources, BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js");
    const { resolveSkillCacheDir } = await import("../home-paths.js");

    const source = BUILT_IN_SOURCES.find((s) => s.id === "schwepps-skills")!;
    await fetchAllSources([source]);

    // The skill id should be schwepps-skills/cached-skill
    const skillId = "schwepps-skills/cached-skill";
    const cacheDir = resolveSkillCacheDir(skillId, sha);
    const cachedPath = path.join(cacheDir, "SKILL.md");

    expect(existsSync(cachedPath)).toBe(true);
    const content = await readFile(cachedPath, "utf-8");
    expect(content).toBe(skillMdContent);
  });

  // -------------------------------------------------------------------------
  // Test 5: skill_files rows inserted for each cached file
  // -------------------------------------------------------------------------
  it("Test 5: skill_files rows are inserted for each cached file with path, kind, and size_bytes", async () => {
    const sha = "aabbccdd1234567890abcdef1234567890abcdef";

    fetchSpy.mockImplementation(async (url: string) => {
      const urlStr = String(url);

      if (urlStr.includes("/git/trees/")) {
        return {
          ok: true,
          json: async () => ({
            tree: [{ path: "files-skill/SKILL.md", type: "blob", size: 350 }],
          }),
        };
      }
      if (urlStr.includes("/commits/")) {
        return {
          ok: true,
          json: async () => ({ sha }),
        };
      }
      if (urlStr.includes("SKILL.md")) {
        return {
          ok: true,
          text: async () => mockSkillMd("Files Skill", "Tests skill_files rows"),
        };
      }
      return { ok: false, status: 404, text: async () => "Not found", json: async () => ({}) };
    });

    const { fetchAllSources, BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js");
    const { getSkillRegistryDb } = await import("../services/skill-registry-db.js");
    const { skillFiles } = await import("../services/skill-registry-schema.js");

    const source = BUILT_IN_SOURCES.find((s) => s.id === "schwepps-skills")!;
    await fetchAllSources([source]);

    const db = await getSkillRegistryDb();
    const files = await db.select().from(skillFiles);
    expect(files.length).toBeGreaterThan(0);

    const skillMdFile = files.find((f) => f.path.endsWith("SKILL.md"));
    expect(skillMdFile).toBeDefined();
    expect(skillMdFile!.kind).toBe("skill");
    expect(skillMdFile!.sizeBytes).toBeGreaterThan(0);
  });

  // -------------------------------------------------------------------------
  // Test 6: Re-fetching with same SHA is idempotent
  // -------------------------------------------------------------------------
  it("Test 6: Re-fetching with same SHA skips re-download (idempotent)", async () => {
    const sha = "idemidemidem1234567890abcdef1234567890ab";

    let fetchCallCount = 0;
    fetchSpy.mockImplementation(async (url: string) => {
      const urlStr = String(url);
      fetchCallCount++;

      if (urlStr.includes("/git/trees/")) {
        return {
          ok: true,
          json: async () => ({
            tree: [{ path: "idem-skill/SKILL.md", type: "blob", size: 100 }],
          }),
        };
      }
      if (urlStr.includes("/commits/")) {
        return {
          ok: true,
          json: async () => ({ sha }),
        };
      }
      if (urlStr.includes("SKILL.md")) {
        return {
          ok: true,
          text: async () => mockSkillMd("Idem Skill", "Tests idempotency"),
        };
      }
      return { ok: false, status: 404, text: async () => "Not found", json: async () => ({}) };
    });

    const { fetchAllSources, BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js");
    const { getSkillRegistryDb } = await import("../services/skill-registry-db.js");
    const { skillVersions } = await import("../services/skill-registry-schema.js");

    const source = BUILT_IN_SOURCES.find((s) => s.id === "schwepps-skills")!;

    // First fetch
    await fetchAllSources([source]);
    const firstCount = fetchCallCount;

    // Second fetch with same SHA — should not re-download SKILL.md
    fetchCallCount = 0;
    await fetchAllSources([source]);

    const db = await getSkillRegistryDb();
    const versions = await db.select().from(skillVersions);

    // Only 1 version row for the same SHA (idempotent insert)
    const idemVersions = versions.filter((v) => v.version === sha);
    expect(idemVersions.length).toBe(1);

    // Second run should have fewer fetch calls (no SKILL.md re-download)
    expect(fetchCallCount).toBeLessThan(firstCount);
  });

  // -------------------------------------------------------------------------
  // Test 7: BUILT_IN_SOURCES has exactly 3 entries
  // -------------------------------------------------------------------------
  it("Test 7: BUILT_IN_SOURCES contains 3 entries (anthropic-official, schwepps-skills, daymade-skills)", async () => {
    const { BUILT_IN_SOURCES } = await import("../services/skill-registry-fetcher.js");

    expect(BUILT_IN_SOURCES).toHaveLength(3);

    const ids = BUILT_IN_SOURCES.map((s) => s.id);
    expect(ids).toContain("anthropic-official");
    expect(ids).toContain("schwepps-skills");
    expect(ids).toContain("daymade-skills");

    // Verify all sources have required fields
    for (const source of BUILT_IN_SOURCES) {
      expect(source.id).toBeTruthy();
      expect(source.type).toMatch(/^(anthropic-marketplace|github-tree)$/);
      expect(source.owner).toBeTruthy();
      expect(source.repo).toBeTruthy();
      expect(source.ref).toBeTruthy();
      expect(source.label).toBeTruthy();
    }
  });
});
