import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { isSkillCollisionError, isSubagentCollisionError, } from "../utils/fetch.js";
import { registrarApi } from "./registrar.js";
// Mock global fetch
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);
describe("registrarApi", () => {
    beforeEach(() => {
        vi.clearAllMocks();
    });
    afterEach(() => {
        vi.clearAllMocks();
    });
    describe("searchPackages", () => {
        it("should return array of packages matching query", async () => {
            const mockPackages = [
                {
                    id: "pkg-1",
                    name: "test-profile",
                    description: "A test profile",
                    authorEmail: "test@example.com",
                    createdAt: "2024-01-01T00:00:00.000Z",
                    updatedAt: "2024-01-01T00:00:00.000Z",
                },
                {
                    id: "pkg-2",
                    name: "another-profile",
                    description: "Another test profile",
                    authorEmail: "test@example.com",
                    createdAt: "2024-01-02T00:00:00.000Z",
                    updatedAt: "2024-01-02T00:00:00.000Z",
                },
            ];
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockPackages),
            });
            const result = await registrarApi.searchPackages({ query: "test" });
            expect(result).toEqual(mockPackages);
            expect(mockFetch).toHaveBeenCalledTimes(1);
            expect(mockFetch).toHaveBeenCalledWith("https://noriskillsets.dev/api/skillsets/search?q=test", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should return empty array when no results", async () => {
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve([]),
            });
            const result = await registrarApi.searchPackages({
                query: "nonexistent",
            });
            expect(result).toEqual([]);
        });
        it("should pass limit and offset query params", async () => {
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve([]),
            });
            await registrarApi.searchPackages({
                query: "test",
                limit: 10,
                offset: 20,
            });
            expect(mockFetch).toHaveBeenCalledWith("https://noriskillsets.dev/api/skillsets/search?q=test&limit=10&offset=20", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should use custom registryUrl when provided", async () => {
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve([]),
            });
            await registrarApi.searchPackages({
                query: "test",
                registryUrl: "https://private-registry.example.com",
            });
            expect(mockFetch).toHaveBeenCalledWith("https://private-registry.example.com/api/skillsets/search?q=test", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should include auth token header when provided", async () => {
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve([]),
            });
            await registrarApi.searchPackages({
                query: "test",
                registryUrl: "https://private-registry.example.com",
                authToken: "test-auth-token",
            });
            expect(mockFetch).toHaveBeenCalledWith("https://private-registry.example.com/api/skillsets/search?q=test", expect.objectContaining({
                method: "GET",
                headers: expect.objectContaining({
                    Authorization: "Bearer test-auth-token",
                }),
            }));
        });
        it("should fall back to /api/profiles/ on 404", async () => {
            const mockPackages = [
                {
                    id: "pkg-1",
                    name: "test-profile",
                    description: "A test profile",
                    authorEmail: "test@example.com",
                    createdAt: "2024-01-01T00:00:00.000Z",
                    updatedAt: "2024-01-01T00:00:00.000Z",
                },
            ];
            mockFetch
                .mockResolvedValueOnce({
                ok: false,
                status: 404,
                json: () => Promise.resolve({ error: "Not found" }),
            })
                .mockResolvedValueOnce({
                ok: true,
                json: () => Promise.resolve(mockPackages),
            });
            const result = await registrarApi.searchPackages({ query: "test" });
            expect(result).toEqual(mockPackages);
            expect(mockFetch).toHaveBeenCalledTimes(2);
            expect(mockFetch).toHaveBeenNthCalledWith(1, "https://noriskillsets.dev/api/skillsets/search?q=test", expect.objectContaining({ method: "GET" }));
            expect(mockFetch).toHaveBeenNthCalledWith(2, "https://noriskillsets.dev/api/profiles/search?q=test", expect.objectContaining({ method: "GET" }));
        });
        it("should not fall back on non-404 errors", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 500,
                json: () => Promise.resolve({ error: "Internal server error" }),
            });
            await expect(registrarApi.searchPackages({ query: "test" })).rejects.toThrow("Internal server error");
            expect(mockFetch).toHaveBeenCalledTimes(1);
        });
        it("should throw error on non-OK response", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 500,
                json: () => Promise.resolve({ error: "Internal server error" }),
            });
            await expect(registrarApi.searchPackages({ query: "test" })).rejects.toThrow("Internal server error");
        });
    });
    describe("getPackument", () => {
        it("should return packument for package name", async () => {
            const mockPackument = {
                name: "test-profile",
                description: "A test profile",
                "dist-tags": { latest: "1.0.0" },
                versions: {
                    "1.0.0": {
                        name: "test-profile",
                        version: "1.0.0",
                        dist: {
                            tarball: "/packages/test-profile/tarball/test-profile-1.0.0.tgz",
                        },
                    },
                },
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockPackument),
            });
            const result = await registrarApi.getPackument({
                packageName: "test-profile",
            });
            expect(result).toEqual(mockPackument);
            expect(mockFetch).toHaveBeenCalledWith("https://noriskillsets.dev/api/skillsets/test-profile", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should throw error when package not found on both endpoints", async () => {
            mockFetch
                .mockResolvedValueOnce({
                ok: false,
                status: 404,
                json: () => Promise.resolve({ error: "Package not found" }),
            })
                .mockResolvedValueOnce({
                ok: false,
                status: 404,
                json: () => Promise.resolve({ error: "Package not found" }),
            });
            await expect(registrarApi.getPackument({ packageName: "nonexistent" })).rejects.toThrow("Package not found");
            expect(mockFetch).toHaveBeenCalledTimes(2);
        });
        it("should fall back to /api/profiles/ on 404", async () => {
            const mockPackument = {
                name: "test-profile",
                "dist-tags": { latest: "1.0.0" },
                versions: {
                    "1.0.0": { name: "test-profile", version: "1.0.0" },
                },
            };
            mockFetch
                .mockResolvedValueOnce({
                ok: false,
                status: 404,
                json: () => Promise.resolve({ error: "Not found" }),
            })
                .mockResolvedValueOnce({
                ok: true,
                json: () => Promise.resolve(mockPackument),
            });
            const result = await registrarApi.getPackument({
                packageName: "test-profile",
            });
            expect(result).toEqual(mockPackument);
            expect(mockFetch).toHaveBeenCalledTimes(2);
            expect(mockFetch).toHaveBeenNthCalledWith(1, "https://noriskillsets.dev/api/skillsets/test-profile", expect.objectContaining({ method: "GET" }));
            expect(mockFetch).toHaveBeenNthCalledWith(2, "https://noriskillsets.dev/api/profiles/test-profile", expect.objectContaining({ method: "GET" }));
        });
        it("should use custom registryUrl when provided", async () => {
            const mockPackument = {
                name: "test-profile",
                "dist-tags": { latest: "1.0.0" },
                versions: {
                    "1.0.0": { name: "test-profile", version: "1.0.0" },
                },
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockPackument),
            });
            await registrarApi.getPackument({
                packageName: "test-profile",
                registryUrl: "https://private-registry.example.com",
            });
            expect(mockFetch).toHaveBeenCalledWith("https://private-registry.example.com/api/skillsets/test-profile", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should include auth token header when provided", async () => {
            const mockPackument = {
                name: "test-profile",
                "dist-tags": { latest: "1.0.0" },
                versions: {
                    "1.0.0": { name: "test-profile", version: "1.0.0" },
                },
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockPackument),
            });
            await registrarApi.getPackument({
                packageName: "test-profile",
                registryUrl: "https://private-registry.example.com",
                authToken: "test-auth-token",
            });
            expect(mockFetch).toHaveBeenCalledWith("https://private-registry.example.com/api/skillsets/test-profile", expect.objectContaining({
                method: "GET",
                headers: expect.objectContaining({
                    Authorization: "Bearer test-auth-token",
                }),
            }));
        });
    });
    describe("downloadTarball", () => {
        it("should return ArrayBuffer on successful download", async () => {
            const mockTarballData = new ArrayBuffer(100);
            mockFetch.mockResolvedValue({
                ok: true,
                arrayBuffer: () => Promise.resolve(mockTarballData),
            });
            const result = await registrarApi.downloadTarball({
                packageName: "test-profile",
                version: "1.0.0",
            });
            expect(result).toBe(mockTarballData);
            expect(mockFetch).toHaveBeenCalledWith("https://noriskillsets.dev/api/skillsets/test-profile/tarball/test-profile-1.0.0.tgz", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should throw error when tarball not found on both endpoints", async () => {
            mockFetch
                .mockResolvedValueOnce({
                ok: false,
                status: 404,
                json: () => Promise.resolve({ error: "Tarball not found" }),
            })
                .mockResolvedValueOnce({
                ok: false,
                status: 404,
                json: () => Promise.resolve({ error: "Tarball not found" }),
            });
            await expect(registrarApi.downloadTarball({
                packageName: "nonexistent",
                version: "1.0.0",
            })).rejects.toThrow("Tarball not found");
            expect(mockFetch).toHaveBeenCalledTimes(2);
        });
        it("should fall back to /api/profiles/ on 404", async () => {
            const mockTarballData = new ArrayBuffer(100);
            mockFetch
                .mockResolvedValueOnce({
                ok: false,
                status: 404,
                json: () => Promise.resolve({ error: "Not found" }),
            })
                .mockResolvedValueOnce({
                ok: true,
                arrayBuffer: () => Promise.resolve(mockTarballData),
            });
            const result = await registrarApi.downloadTarball({
                packageName: "test-profile",
                version: "1.0.0",
            });
            expect(result).toBe(mockTarballData);
            expect(mockFetch).toHaveBeenCalledTimes(2);
            expect(mockFetch).toHaveBeenNthCalledWith(1, "https://noriskillsets.dev/api/skillsets/test-profile/tarball/test-profile-1.0.0.tgz", expect.objectContaining({ method: "GET" }));
            expect(mockFetch).toHaveBeenNthCalledWith(2, "https://noriskillsets.dev/api/profiles/test-profile/tarball/test-profile-1.0.0.tgz", expect.objectContaining({ method: "GET" }));
        });
        it("should fetch latest version when no version specified", async () => {
            // First call to getPackument to resolve latest version
            const mockPackument = {
                name: "test-profile",
                "dist-tags": { latest: "2.0.0" },
                versions: {
                    "1.0.0": { name: "test-profile", version: "1.0.0" },
                    "2.0.0": { name: "test-profile", version: "2.0.0" },
                },
            };
            const mockTarballData = new ArrayBuffer(100);
            mockFetch
                .mockResolvedValueOnce({
                ok: true,
                json: () => Promise.resolve(mockPackument),
            })
                .mockResolvedValueOnce({
                ok: true,
                arrayBuffer: () => Promise.resolve(mockTarballData),
            });
            const result = await registrarApi.downloadTarball({
                packageName: "test-profile",
            });
            expect(result).toBe(mockTarballData);
            // First call should be to get packument
            expect(mockFetch).toHaveBeenNthCalledWith(1, "https://noriskillsets.dev/api/skillsets/test-profile", expect.objectContaining({
                method: "GET",
            }));
            // Second call should be to download tarball with resolved version
            expect(mockFetch).toHaveBeenNthCalledWith(2, "https://noriskillsets.dev/api/skillsets/test-profile/tarball/test-profile-2.0.0.tgz", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should use custom registryUrl when provided", async () => {
            const mockTarballData = new ArrayBuffer(100);
            mockFetch.mockResolvedValue({
                ok: true,
                arrayBuffer: () => Promise.resolve(mockTarballData),
            });
            await registrarApi.downloadTarball({
                packageName: "test-profile",
                version: "1.0.0",
                registryUrl: "https://private-registry.example.com",
            });
            expect(mockFetch).toHaveBeenCalledWith("https://private-registry.example.com/api/skillsets/test-profile/tarball/test-profile-1.0.0.tgz", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should include auth token header when provided", async () => {
            const mockTarballData = new ArrayBuffer(100);
            mockFetch.mockResolvedValue({
                ok: true,
                arrayBuffer: () => Promise.resolve(mockTarballData),
            });
            await registrarApi.downloadTarball({
                packageName: "test-profile",
                version: "1.0.0",
                registryUrl: "https://private-registry.example.com",
                authToken: "test-auth-token",
            });
            expect(mockFetch).toHaveBeenCalledWith("https://private-registry.example.com/api/skillsets/test-profile/tarball/test-profile-1.0.0.tgz", expect.objectContaining({
                method: "GET",
                headers: expect.objectContaining({
                    Authorization: "Bearer test-auth-token",
                }),
            }));
        });
        it("should pass registryUrl and authToken when resolving latest version", async () => {
            const mockPackument = {
                name: "test-profile",
                "dist-tags": { latest: "2.0.0" },
                versions: {
                    "2.0.0": { name: "test-profile", version: "2.0.0" },
                },
            };
            const mockTarballData = new ArrayBuffer(100);
            mockFetch
                .mockResolvedValueOnce({
                ok: true,
                json: () => Promise.resolve(mockPackument),
            })
                .mockResolvedValueOnce({
                ok: true,
                arrayBuffer: () => Promise.resolve(mockTarballData),
            });
            await registrarApi.downloadTarball({
                packageName: "test-profile",
                registryUrl: "https://private-registry.example.com",
                authToken: "test-auth-token",
            });
            // First call should be to get packument with auth
            expect(mockFetch).toHaveBeenNthCalledWith(1, "https://private-registry.example.com/api/skillsets/test-profile", expect.objectContaining({
                method: "GET",
                headers: expect.objectContaining({
                    Authorization: "Bearer test-auth-token",
                }),
            }));
            // Second call should be to download tarball with auth
            expect(mockFetch).toHaveBeenNthCalledWith(2, "https://private-registry.example.com/api/skillsets/test-profile/tarball/test-profile-2.0.0.tgz", expect.objectContaining({
                method: "GET",
                headers: expect.objectContaining({
                    Authorization: "Bearer test-auth-token",
                }),
            }));
        });
    });
    describe("searchPackagesOnRegistry", () => {
        it("should search packages on a custom registry URL without auth", async () => {
            const mockPackages = [
                {
                    id: "pkg-1",
                    name: "custom-profile",
                    description: "A custom profile",
                    authorEmail: "test@example.com",
                    createdAt: "2024-01-01T00:00:00.000Z",
                    updatedAt: "2024-01-01T00:00:00.000Z",
                },
            ];
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockPackages),
            });
            const result = await registrarApi.searchPackagesOnRegistry({
                query: "custom",
                registryUrl: "https://custom.registry.com",
            });
            expect(result).toEqual(mockPackages);
            expect(mockFetch).toHaveBeenCalledWith("https://custom.registry.com/api/skillsets/search?q=custom", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should include Authorization header when authToken is provided", async () => {
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve([]),
            });
            await registrarApi.searchPackagesOnRegistry({
                query: "test",
                registryUrl: "https://private.registry.com",
                authToken: "secret-token-123",
            });
            expect(mockFetch).toHaveBeenCalledWith("https://private.registry.com/api/skillsets/search?q=test", expect.objectContaining({
                method: "GET",
                headers: expect.objectContaining({
                    Authorization: "Bearer secret-token-123",
                }),
            }));
        });
        it("should not include Authorization header when authToken is null", async () => {
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve([]),
            });
            await registrarApi.searchPackagesOnRegistry({
                query: "test",
                registryUrl: "https://public.registry.com",
                authToken: null,
            });
            const callArgs = mockFetch.mock.calls[0];
            expect(callArgs[1].headers).toEqual({});
        });
        it("should pass limit and offset query params", async () => {
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve([]),
            });
            await registrarApi.searchPackagesOnRegistry({
                query: "test",
                registryUrl: "https://custom.registry.com",
                limit: 5,
                offset: 10,
            });
            expect(mockFetch).toHaveBeenCalledWith("https://custom.registry.com/api/skillsets/search?q=test&limit=5&offset=10", expect.anything());
        });
        it("should fall back to /api/profiles/ on 404", async () => {
            const mockPackages = [
                {
                    id: "pkg-1",
                    name: "custom-profile",
                    description: "A custom profile",
                    authorEmail: "test@example.com",
                    createdAt: "2024-01-01T00:00:00.000Z",
                    updatedAt: "2024-01-01T00:00:00.000Z",
                },
            ];
            mockFetch
                .mockResolvedValueOnce({
                ok: false,
                status: 404,
                json: () => Promise.resolve({ error: "Not found" }),
            })
                .mockResolvedValueOnce({
                ok: true,
                json: () => Promise.resolve(mockPackages),
            });
            const result = await registrarApi.searchPackagesOnRegistry({
                query: "custom",
                registryUrl: "https://custom.registry.com",
            });
            expect(result).toEqual(mockPackages);
            expect(mockFetch).toHaveBeenCalledTimes(2);
            expect(mockFetch).toHaveBeenNthCalledWith(1, "https://custom.registry.com/api/skillsets/search?q=custom", expect.objectContaining({ method: "GET" }));
            expect(mockFetch).toHaveBeenNthCalledWith(2, "https://custom.registry.com/api/profiles/search?q=custom", expect.objectContaining({ method: "GET" }));
        });
        it("should throw error on non-OK response", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 401,
                json: () => Promise.resolve({ error: "Unauthorized" }),
            });
            await expect(registrarApi.searchPackagesOnRegistry({
                query: "test",
                registryUrl: "https://private.registry.com",
                authToken: "invalid-token",
            })).rejects.toThrow("Unauthorized");
        });
    });
    describe("uploadSkillset", () => {
        it("should upload skillset with multipart form data", async () => {
            const mockResponse = {
                name: "test-profile",
                version: "1.0.0",
                description: "Test description",
                tarballSha: "sha512-abc123",
                createdAt: "2024-01-01T00:00:00.000Z",
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockResponse),
            });
            const archiveData = new ArrayBuffer(100);
            const result = await registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
            });
            expect(result).toEqual(mockResponse);
            expect(mockFetch).toHaveBeenCalledWith("https://noriskillsets.dev/api/skillsets/test-profile/skillset", expect.objectContaining({
                method: "PUT",
                headers: expect.objectContaining({
                    Authorization: "Bearer test-token",
                }),
            }));
            // Verify the body is FormData
            const callArgs = mockFetch.mock.calls[0];
            expect(callArgs[1].body).toBeInstanceOf(FormData);
        });
        it("should include description in form data when provided", async () => {
            const mockResponse = {
                name: "test-profile",
                version: "1.0.0",
                description: "Custom description",
                tarballSha: "sha512-abc123",
                createdAt: "2024-01-01T00:00:00.000Z",
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockResponse),
            });
            const archiveData = new ArrayBuffer(100);
            await registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                description: "Custom description",
                authToken: "test-token",
            });
            // Verify FormData includes description
            const callArgs = mockFetch.mock.calls[0];
            const formData = callArgs[1].body;
            expect(formData.get("description")).toBe("Custom description");
        });
        it("should throw error on unauthorized (401)", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 401,
                json: () => Promise.resolve({ error: "Unauthorized" }),
            });
            const archiveData = new ArrayBuffer(100);
            await expect(registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "invalid-token",
            })).rejects.toThrow("Unauthorized");
        });
        it("should throw error on forbidden (403)", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 403,
                json: () => Promise.resolve({
                    error: "You are not a maintainer of package test-profile",
                }),
            });
            const archiveData = new ArrayBuffer(100);
            await expect(registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
            })).rejects.toThrow("You are not a maintainer");
        });
        it("should throw error on version conflict (409)", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 409,
                json: () => Promise.resolve({
                    error: "Version 1.0.0 already exists for package test-profile",
                }),
            });
            const archiveData = new ArrayBuffer(100);
            await expect(registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
            })).rejects.toThrow("already exists");
        });
        it("should throw error on validation failure (400)", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 400,
                json: () => Promise.resolve({ error: "Profile must contain CLAUDE.md" }),
            });
            const archiveData = new ArrayBuffer(100);
            await expect(registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
            })).rejects.toThrow("CLAUDE.md");
        });
        it("should use custom registryUrl when provided", async () => {
            const mockResponse = {
                name: "test-profile",
                version: "1.0.0",
                tarballSha: "sha512-abc123",
                createdAt: "2024-01-01T00:00:00.000Z",
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockResponse),
            });
            const archiveData = new ArrayBuffer(100);
            await registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
                registryUrl: "https://private-registry.example.com",
            });
            expect(mockFetch).toHaveBeenCalledWith("https://private-registry.example.com/api/skillsets/test-profile/skillset", expect.objectContaining({
                method: "PUT",
                headers: expect.objectContaining({
                    Authorization: "Bearer test-token",
                }),
            }));
        });
        it("should default to REGISTRAR_URL when registryUrl not provided", async () => {
            const mockResponse = {
                name: "test-profile",
                version: "1.0.0",
                tarballSha: "sha512-abc123",
                createdAt: "2024-01-01T00:00:00.000Z",
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockResponse),
            });
            const archiveData = new ArrayBuffer(100);
            await registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
            });
            expect(mockFetch).toHaveBeenCalledWith("https://noriskillsets.dev/api/skillsets/test-profile/skillset", expect.anything());
        });
        it("should fall back to /api/profiles/ on 404", async () => {
            const mockResponse = {
                name: "test-profile",
                version: "1.0.0",
                tarballSha: "sha512-abc123",
                createdAt: "2024-01-01T00:00:00.000Z",
            };
            mockFetch
                .mockResolvedValueOnce({
                ok: false,
                status: 404,
                json: () => Promise.resolve({ error: "Not found" }),
            })
                .mockResolvedValueOnce({
                ok: true,
                json: () => Promise.resolve(mockResponse),
            });
            const archiveData = new ArrayBuffer(100);
            const result = await registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
            });
            expect(result).toEqual(mockResponse);
            expect(mockFetch).toHaveBeenCalledTimes(2);
            expect(mockFetch).toHaveBeenNthCalledWith(1, "https://noriskillsets.dev/api/skillsets/test-profile/skillset", expect.objectContaining({ method: "PUT" }));
            expect(mockFetch).toHaveBeenNthCalledWith(2, "https://noriskillsets.dev/api/profiles/test-profile/profile", expect.objectContaining({ method: "PUT" }));
        });
    });
    // Skill API tests
    describe("searchSkills", () => {
        it("should return array of skills matching query", async () => {
            const mockSkills = [
                {
                    id: "skill-1",
                    name: "writing-plans",
                    description: "A skill for writing plans",
                    authorEmail: "test@example.com",
                    createdAt: "2024-01-01T00:00:00.000Z",
                    updatedAt: "2024-01-01T00:00:00.000Z",
                },
            ];
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockSkills),
            });
            const result = await registrarApi.searchSkills({ query: "writing" });
            expect(result).toEqual(mockSkills);
            expect(mockFetch).toHaveBeenCalledWith("https://noriskillsets.dev/api/skills/search?q=writing", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should use custom registryUrl when provided", async () => {
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve([]),
            });
            await registrarApi.searchSkills({
                query: "test",
                registryUrl: "https://private-registry.example.com",
            });
            expect(mockFetch).toHaveBeenCalledWith("https://private-registry.example.com/api/skills/search?q=test", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should include auth token header when provided", async () => {
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve([]),
            });
            await registrarApi.searchSkills({
                query: "test",
                authToken: "test-auth-token",
            });
            expect(mockFetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
                headers: expect.objectContaining({
                    Authorization: "Bearer test-auth-token",
                }),
            }));
        });
    });
    describe("getSkillPackument", () => {
        it("should return packument for skill name", async () => {
            const mockPackument = {
                name: "writing-plans",
                description: "A skill for writing plans",
                "dist-tags": { latest: "1.0.0" },
                versions: {
                    "1.0.0": {
                        name: "writing-plans",
                        version: "1.0.0",
                        dist: {
                            tarball: "/skills/writing-plans/tarball/writing-plans-1.0.0.tgz",
                        },
                    },
                },
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockPackument),
            });
            const result = await registrarApi.getSkillPackument({
                skillName: "writing-plans",
            });
            expect(result).toEqual(mockPackument);
            expect(mockFetch).toHaveBeenCalledWith("https://noriskillsets.dev/api/skills/writing-plans", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should throw error when skill not found", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 404,
                json: () => Promise.resolve({ error: "Skill not found" }),
            });
            await expect(registrarApi.getSkillPackument({ skillName: "nonexistent" })).rejects.toThrow("Skill not found");
        });
    });
    describe("downloadSkillTarball", () => {
        it("should return ArrayBuffer on successful download", async () => {
            const mockTarballData = new ArrayBuffer(100);
            mockFetch.mockResolvedValue({
                ok: true,
                arrayBuffer: () => Promise.resolve(mockTarballData),
            });
            const result = await registrarApi.downloadSkillTarball({
                skillName: "writing-plans",
                version: "1.0.0",
            });
            expect(result).toBe(mockTarballData);
            expect(mockFetch).toHaveBeenCalledWith("https://noriskillsets.dev/api/skills/writing-plans/tarball/writing-plans-1.0.0.tgz", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should fetch latest version when no version specified", async () => {
            const mockPackument = {
                name: "writing-plans",
                "dist-tags": { latest: "2.0.0" },
                versions: {
                    "1.0.0": { name: "writing-plans", version: "1.0.0" },
                    "2.0.0": { name: "writing-plans", version: "2.0.0" },
                },
            };
            const mockTarballData = new ArrayBuffer(100);
            mockFetch
                .mockResolvedValueOnce({
                ok: true,
                json: () => Promise.resolve(mockPackument),
            })
                .mockResolvedValueOnce({
                ok: true,
                arrayBuffer: () => Promise.resolve(mockTarballData),
            });
            const result = await registrarApi.downloadSkillTarball({
                skillName: "writing-plans",
            });
            expect(result).toBe(mockTarballData);
            // First call should be to get packument
            expect(mockFetch).toHaveBeenNthCalledWith(1, "https://noriskillsets.dev/api/skills/writing-plans", expect.objectContaining({
                method: "GET",
            }));
            // Second call should be to download tarball with resolved version
            expect(mockFetch).toHaveBeenNthCalledWith(2, "https://noriskillsets.dev/api/skills/writing-plans/tarball/writing-plans-2.0.0.tgz", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should throw error when tarball not found", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 404,
                json: () => Promise.resolve({ error: "Tarball not found" }),
            });
            await expect(registrarApi.downloadSkillTarball({
                skillName: "nonexistent",
                version: "1.0.0",
            })).rejects.toThrow("Tarball not found");
        });
    });
    describe("uploadSkill", () => {
        it("should upload skill with multipart form data", async () => {
            const mockResponse = {
                name: "writing-plans",
                version: "1.0.0",
                description: "A skill for writing plans",
                tarballSha: "sha512-abc123",
                createdAt: "2024-01-01T00:00:00.000Z",
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockResponse),
            });
            const archiveData = new ArrayBuffer(100);
            const result = await registrarApi.uploadSkill({
                skillName: "writing-plans",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
            });
            expect(result).toEqual(mockResponse);
            expect(mockFetch).toHaveBeenCalledWith("https://noriskillsets.dev/api/skills/writing-plans/skill", expect.objectContaining({
                method: "PUT",
                headers: expect.objectContaining({
                    Authorization: "Bearer test-token",
                }),
            }));
            // Verify the body is FormData
            const callArgs = mockFetch.mock.calls[0];
            expect(callArgs[1].body).toBeInstanceOf(FormData);
        });
        it("should throw error on unauthorized (401)", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 401,
                json: () => Promise.resolve({ error: "Unauthorized" }),
            });
            const archiveData = new ArrayBuffer(100);
            await expect(registrarApi.uploadSkill({
                skillName: "writing-plans",
                version: "1.0.0",
                archiveData,
                authToken: "invalid-token",
            })).rejects.toThrow("Unauthorized");
        });
    });
    describe("uploadSkillset skill collision handling", () => {
        it("should throw SkillCollisionError when 409 response contains conflicts array", async () => {
            const conflictResponse = {
                error: "Skill conflicts detected: skill-a, skill-b. Resolution required.",
                conflicts: [
                    {
                        skillId: "skill-a",
                        exists: true,
                        canPublish: true,
                        latestVersion: "1.0.0",
                        owner: "user@example.com",
                        availableActions: ["cancel", "namespace", "link", "updateVersion"],
                        contentUnchanged: true,
                    },
                    {
                        skillId: "skill-b",
                        exists: true,
                        canPublish: false,
                        latestVersion: "2.0.0",
                        owner: "other@example.com",
                        availableActions: ["cancel", "namespace", "link"],
                        contentUnchanged: false,
                    },
                ],
                requiresVersions: true,
            };
            mockFetch.mockResolvedValue({
                ok: false,
                status: 409,
                json: () => Promise.resolve(conflictResponse),
            });
            const archiveData = new ArrayBuffer(100);
            await expect(registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
            })).rejects.toThrow("Skill conflicts detected");
            // Verify it throws SkillCollisionError with correct properties
            try {
                await registrarApi.uploadSkillset({
                    packageName: "test-profile",
                    version: "1.0.0",
                    archiveData,
                    authToken: "test-token",
                });
            }
            catch (err) {
                expect(isSkillCollisionError(err)).toBe(true);
                if (isSkillCollisionError(err)) {
                    expect(err.conflicts).toHaveLength(2);
                    expect(err.conflicts[0].skillId).toBe("skill-a");
                    expect(err.conflicts[0].contentUnchanged).toBe(true);
                    expect(err.conflicts[1].skillId).toBe("skill-b");
                    expect(err.conflicts[1].contentUnchanged).toBe(false);
                    expect(err.requiresVersions).toBe(true);
                }
            }
        });
        it("should throw regular ApiError when 409 response has no conflicts array", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 409,
                json: () => Promise.resolve({
                    error: "Version 1.0.0 already exists for package test-profile",
                }),
            });
            const archiveData = new ArrayBuffer(100);
            await expect(registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
            })).rejects.toThrow("already exists");
            // Verify it does NOT throw SkillCollisionError
            try {
                await registrarApi.uploadSkillset({
                    packageName: "test-profile",
                    version: "1.0.0",
                    archiveData,
                    authToken: "test-token",
                });
            }
            catch (err) {
                expect(isSkillCollisionError(err)).toBe(false);
            }
        });
        it("should throw regular ApiError when 409 response has empty conflicts array", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 409,
                json: () => Promise.resolve({
                    error: "Version conflict",
                    conflicts: [],
                }),
            });
            const archiveData = new ArrayBuffer(100);
            try {
                await registrarApi.uploadSkillset({
                    packageName: "test-profile",
                    version: "1.0.0",
                    archiveData,
                    authToken: "test-token",
                });
            }
            catch (err) {
                expect(isSkillCollisionError(err)).toBe(false);
            }
        });
        it("should include resolutionStrategy in FormData when provided", async () => {
            const mockResponse = {
                name: "test-profile",
                version: "1.0.0",
                tarballSha: "sha512-abc123",
                createdAt: "2024-01-01T00:00:00.000Z",
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockResponse),
            });
            const archiveData = new ArrayBuffer(100);
            const resolutionStrategy = {
                "skill-a": { action: "link" },
                "skill-b": { action: "namespace" },
            };
            await registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
                resolutionStrategy,
            });
            // Verify FormData includes resolutionStrategy as JSON string
            const callArgs = mockFetch.mock.calls[0];
            const formData = callArgs[1].body;
            expect(formData.get("resolutionStrategy")).toBe(JSON.stringify(resolutionStrategy));
        });
        it("should not include resolutionStrategy in FormData when null", async () => {
            const mockResponse = {
                name: "test-profile",
                version: "1.0.0",
                tarballSha: "sha512-abc123",
                createdAt: "2024-01-01T00:00:00.000Z",
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockResponse),
            });
            const archiveData = new ArrayBuffer(100);
            await registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
                resolutionStrategy: null,
            });
            // Verify FormData does NOT include resolutionStrategy
            const callArgs = mockFetch.mock.calls[0];
            const formData = callArgs[1].body;
            expect(formData.get("resolutionStrategy")).toBeNull();
        });
        it("should include subagentResolutionStrategy in FormData when provided", async () => {
            const mockResponse = {
                name: "test-profile",
                version: "1.0.0",
                tarballSha: "sha512-abc123",
                createdAt: "2024-01-01T00:00:00.000Z",
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockResponse),
            });
            const archiveData = new ArrayBuffer(100);
            const subagentResolutionStrategy = {
                "my-subagent": { action: "link" },
            };
            await registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
                subagentResolutionStrategy,
            });
            const callArgs = mockFetch.mock.calls[0];
            const formData = callArgs[1].body;
            expect(formData.get("subagentResolutionStrategy")).toBe(JSON.stringify(subagentResolutionStrategy));
        });
        it("should throw SubagentCollisionError when 409 conflicts array contains subagentId items", async () => {
            // The registrar server serializes both SkillCollisionError and
            // SubagentCollisionError into a response body with a single `conflicts`
            // key. The client must discriminate by item shape (subagentId vs skillId).
            const conflictResponse = {
                error: "Subagent conflicts detected: my-subagent. Resolution required.",
                conflicts: [
                    {
                        subagentId: "my-subagent",
                        exists: true,
                        canPublish: false,
                        latestVersion: "1.0.0",
                        owner: "other@example.com",
                        availableActions: ["cancel", "namespace", "link"],
                        contentUnchanged: false,
                    },
                ],
                requiresVersions: true,
            };
            mockFetch.mockResolvedValue({
                ok: false,
                status: 409,
                json: () => Promise.resolve(conflictResponse),
            });
            const archiveData = new ArrayBuffer(100);
            await expect(registrarApi.uploadSkillset({
                packageName: "test-profile",
                version: "1.0.0",
                archiveData,
                authToken: "test-token",
            })).rejects.toThrow("Subagent conflicts detected");
            try {
                await registrarApi.uploadSkillset({
                    packageName: "test-profile",
                    version: "1.0.0",
                    archiveData,
                    authToken: "test-token",
                });
            }
            catch (err) {
                expect(isSubagentCollisionError(err)).toBe(true);
                expect(isSkillCollisionError(err)).toBe(false);
                if (isSubagentCollisionError(err)) {
                    expect(err.conflicts).toHaveLength(1);
                    expect(err.conflicts[0].subagentId).toBe("my-subagent");
                    expect(err.conflicts[0].contentUnchanged).toBe(false);
                    expect(err.requiresVersions).toBe(true);
                }
            }
        });
        it("should throw SkillCollisionError when 409 conflicts array contains both skillId and subagentId items", async () => {
            // Defensive behavior: if an item has a skillId, prefer SkillCollisionError
            // even when other items have subagentId. This mirrors the web UI's
            // "some" predicate and keeps existing skill-resolution flows working.
            const conflictResponse = {
                error: "Conflicts detected. Resolution required.",
                conflicts: [
                    {
                        skillId: "skill-a",
                        exists: true,
                        canPublish: true,
                        latestVersion: "1.0.0",
                        owner: "user@example.com",
                        availableActions: ["cancel", "namespace", "link", "updateVersion"],
                        contentUnchanged: true,
                    },
                    {
                        subagentId: "subagent-b",
                        exists: true,
                        canPublish: true,
                        latestVersion: "1.0.0",
                        owner: "user@example.com",
                        availableActions: ["cancel", "namespace", "link"],
                        contentUnchanged: true,
                    },
                ],
            };
            mockFetch.mockResolvedValue({
                ok: false,
                status: 409,
                json: () => Promise.resolve(conflictResponse),
            });
            const archiveData = new ArrayBuffer(100);
            expect.assertions(2);
            try {
                await registrarApi.uploadSkillset({
                    packageName: "test-profile",
                    version: "1.0.0",
                    archiveData,
                    authToken: "test-token",
                });
            }
            catch (err) {
                expect(isSkillCollisionError(err)).toBe(true);
                expect(isSubagentCollisionError(err)).toBe(false);
            }
        });
    });
    // Subagent API tests
    describe("getSubagentPackument", () => {
        it("should fetch subagent packument from /api/subagents/:name", async () => {
            const mockPackument = {
                name: "my-subagent",
                description: "A test subagent",
                "dist-tags": { latest: "1.0.0" },
                versions: {
                    "1.0.0": {
                        name: "my-subagent",
                        version: "1.0.0",
                        dist: {
                            tarball: "/subagents/my-subagent/tarball/my-subagent-1.0.0.tgz",
                        },
                    },
                },
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockPackument),
            });
            const result = await registrarApi.getSubagentPackument({
                subagentName: "my-subagent",
            });
            expect(result).toEqual(mockPackument);
            expect(mockFetch).toHaveBeenCalledWith("https://noriskillsets.dev/api/subagents/my-subagent", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should use custom registryUrl when provided", async () => {
            const mockPackument = {
                name: "my-subagent",
                "dist-tags": { latest: "1.0.0" },
                versions: {
                    "1.0.0": { name: "my-subagent", version: "1.0.0" },
                },
            };
            mockFetch.mockResolvedValue({
                ok: true,
                json: () => Promise.resolve(mockPackument),
            });
            await registrarApi.getSubagentPackument({
                subagentName: "my-subagent",
                registryUrl: "https://custom.registry",
            });
            expect(mockFetch).toHaveBeenCalledWith("https://custom.registry/api/subagents/my-subagent", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should throw error when subagent not found", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 404,
                json: () => Promise.resolve({ error: "Subagent not found" }),
            });
            await expect(registrarApi.getSubagentPackument({ subagentName: "nonexistent" })).rejects.toThrow("Subagent not found");
        });
    });
    describe("downloadSubagentTarball", () => {
        it("should resolve latest version from packument when no version specified", async () => {
            const mockPackument = {
                name: "my-subagent",
                "dist-tags": { latest: "1.0.0" },
                versions: {
                    "1.0.0": { name: "my-subagent", version: "1.0.0" },
                },
            };
            const mockTarballData = new ArrayBuffer(100);
            mockFetch
                .mockResolvedValueOnce({
                ok: true,
                json: () => Promise.resolve(mockPackument),
            })
                .mockResolvedValueOnce({
                ok: true,
                arrayBuffer: () => Promise.resolve(mockTarballData),
            });
            const result = await registrarApi.downloadSubagentTarball({
                subagentName: "my-subagent",
            });
            expect(result).toBe(mockTarballData);
            // First call should be to get packument
            expect(mockFetch).toHaveBeenNthCalledWith(1, "https://noriskillsets.dev/api/subagents/my-subagent", expect.objectContaining({
                method: "GET",
            }));
            // Second call should be to download tarball with resolved version
            expect(mockFetch).toHaveBeenNthCalledWith(2, "https://noriskillsets.dev/api/subagents/my-subagent/tarball/my-subagent-1.0.0.tgz", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should use specified version", async () => {
            const mockTarballData = new ArrayBuffer(100);
            mockFetch.mockResolvedValue({
                ok: true,
                arrayBuffer: () => Promise.resolve(mockTarballData),
            });
            const result = await registrarApi.downloadSubagentTarball({
                subagentName: "my-subagent",
                version: "2.0.0",
            });
            expect(result).toBe(mockTarballData);
            expect(mockFetch).toHaveBeenCalledWith("https://noriskillsets.dev/api/subagents/my-subagent/tarball/my-subagent-2.0.0.tgz", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should return ArrayBuffer on successful download", async () => {
            const mockTarballData = new ArrayBuffer(100);
            mockFetch.mockResolvedValue({
                ok: true,
                arrayBuffer: () => Promise.resolve(mockTarballData),
            });
            const result = await registrarApi.downloadSubagentTarball({
                subagentName: "my-subagent",
                version: "1.0.0",
            });
            expect(result).toBe(mockTarballData);
            expect(mockFetch).toHaveBeenCalledWith("https://noriskillsets.dev/api/subagents/my-subagent/tarball/my-subagent-1.0.0.tgz", expect.objectContaining({
                method: "GET",
            }));
        });
        it("should throw error when tarball not found", async () => {
            mockFetch.mockResolvedValue({
                ok: false,
                status: 404,
                json: () => Promise.resolve({ error: "Tarball not found" }),
            });
            await expect(registrarApi.downloadSubagentTarball({
                subagentName: "nonexistent",
                version: "1.0.0",
            })).rejects.toThrow("Tarball not found");
        });
    });
});
//# sourceMappingURL=registrar.test.js.map