/**
 * Tests for upload flow
 *
 * Tests the interactive upload flow including batch conflict resolution,
 * use existing option, and one-by-one resolution.
 */
import * as clack from "@clack/prompts";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { uploadFlow } from "./upload.js";
// Mock @clack/prompts
vi.mock("@clack/prompts", () => ({
    intro: vi.fn(),
    outro: vi.fn(),
    select: vi.fn(),
    text: vi.fn(),
    spinner: vi.fn(() => ({
        start: vi.fn(),
        stop: vi.fn(),
        message: vi.fn(),
    })),
    note: vi.fn(),
    log: {
        info: vi.fn(),
        success: vi.fn(),
        warn: vi.fn(),
        error: vi.fn(),
        message: vi.fn(),
    },
    isCancel: vi.fn(() => false),
    cancel: vi.fn(),
}));
// Cancel symbol used by clack
const cancelSymbol = Symbol.for("cancel");
/**
 * Helper to create a mock callbacks object for uploadFlow
 * @param args - The function arguments
 * @param args.version - Version to return from onDetermineVersion
 * @param args.uploadResults - Sequence of upload results to return
 *
 * @returns Mock callbacks for uploadFlow
 */
const createMockCallbacks = (args) => {
    const { version, uploadResults } = args;
    const onUpload = vi.fn();
    for (const result of uploadResults) {
        onUpload.mockResolvedValueOnce(result);
    }
    return {
        onDetermineVersion: vi.fn().mockResolvedValue({
            version: version ?? "1.0.0",
            isNewPackage: false,
        }),
        onUpload,
    };
};
/**
 * Helper to get text content passed to clack.note
 *
 * @returns Array of note content strings
 */
const getNoteContent = () => {
    return vi
        .mocked(clack.note)
        .mock.calls.map((call) => `${call[0] ?? ""} ${call[1] ?? ""}`);
};
/**
 * Helper to get notes whose title starts with "Files changed for". Used to
 * scope assertions to the per-conflict file-change display, so unrelated
 * notes (summaries, diagnostics) cannot accidentally satisfy or fail a test.
 *
 * @returns Array of { body, title } for each matching note
 */
const getFileChangeNotes = () => {
    return vi
        .mocked(clack.note)
        .mock.calls.filter((call) => String(call[1] ?? "").startsWith("Files changed for"))
        .map((call) => ({
        body: String(call[0] ?? ""),
        title: String(call[1] ?? ""),
    }));
};
describe("uploadFlow", () => {
    beforeEach(() => {
        vi.clearAllMocks();
    });
    afterEach(() => {
        vi.mocked(clack.select).mockReset();
        vi.mocked(clack.text).mockReset();
        vi.mocked(clack.isCancel).mockReturnValue(false);
    });
    it("should include statusMessage in successful result", async () => {
        const callbacks = createMockCallbacks({
            uploadResults: [
                {
                    success: true,
                    version: "1.0.0",
                    extractedSkills: { succeeded: [], failed: [] },
                },
            ],
        });
        const result = await uploadFlow({
            profileDisplayName: "myorg/my-profile",
            skillsetName: "my-profile",
            registryUrl: "https://myorg.noriskillsets.dev",
            callbacks,
        });
        expect(result).not.toBeNull();
        expect(result.statusMessage).toContain("myorg/my-profile");
    });
    describe("use existing option", () => {
        it("should show use existing option for changed skills", async () => {
            const conflicts = [
                {
                    skillId: "changed-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    owner: "me@example.com",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            // User selects "link" (which is the "Use Existing" option for changed skills)
            vi.mocked(clack.select).mockResolvedValueOnce("link");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // Verify the select prompt included a "Use Existing" labeled option
            const selectCall = vi.mocked(clack.select).mock.calls[0][0];
            const options = selectCall.options;
            const useExistingOption = options.find((o) => o.value === "link");
            expect(useExistingOption).toBeDefined();
            expect(useExistingOption?.label).toBe("Use Existing");
            expect(useExistingOption?.hint).toContain("discard any local changes");
        });
        it("should NOT show use existing option for unchanged skills", async () => {
            const conflicts = [
                {
                    skillId: "unchanged-skill",
                    exists: true,
                    canPublish: false,
                    latestVersion: "1.0.0",
                    owner: "other@example.com",
                    availableActions: ["cancel", "namespace", "link"],
                    contentUnchanged: true,
                },
            ];
            // This conflict is auto-resolved, so no select prompt should appear
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // Auto-resolved — no select prompt should have been called
            expect(clack.select).not.toHaveBeenCalled();
        });
        it("should show 'Use Existing' label for changed skills when link is available in one-by-one mode", async () => {
            // A changed skill that has link in availableActions (content changed, so "Use Existing" with discard warning)
            // + an unchanged skill without link in availableActions (so it goes to interactive with only namespace/updateVersion)
            const conflicts = [
                {
                    skillId: "changed-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
                {
                    skillId: "unchanged-no-link",
                    exists: true,
                    canPublish: true,
                    latestVersion: "2.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion"],
                    contentUnchanged: true,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            // Batch prompt: "Choose one-by-one"
            vi.mocked(clack.select)
                .mockResolvedValueOnce("one-by-one") // batch prompt
                .mockResolvedValueOnce("namespace") // first conflict
                .mockResolvedValueOnce("namespace"); // second conflict
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // The first conflict (changed) should have "Use Existing" for the link option
            const firstConflictCall = vi.mocked(clack.select).mock.calls[1][0];
            const firstOptions = firstConflictCall.options;
            const useExistingOption = firstOptions.find((o) => o.value === "link");
            expect(useExistingOption).toBeDefined();
            expect(useExistingOption?.label).toBe("Use Existing");
            expect(useExistingOption?.hint).toContain("discard any local changes");
        });
        it("should record canonical SKILL.md content for replacement when 'use existing' is picked on a changed skill", async () => {
            const remoteSkillMd = "# canonical SKILL.md from registry\n";
            const conflicts = [
                {
                    skillId: "diverged-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    existingSkillMd: remoteSkillMd,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: {
                            succeeded: [{ name: "diverged-skill", version: "1.0.0" }],
                            failed: [],
                        },
                    },
                ],
            });
            // User picks "link" (use existing) — discards local changes
            vi.mocked(clack.select).mockResolvedValueOnce("link");
            const result = await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            expect(result).not.toBeNull();
            // Sync layer must overwrite the local file with this exact content,
            // so the next upload doesn't re-detect the same conflict.
            expect(result.linkedSkillsToReplace.get("diverged-skill")).toBe(remoteSkillMd);
            expect(result.linkedSkillVersions.get("diverged-skill")).toBe("1.0.0");
        });
        it("should NOT record SKILL.md replacement when content was unchanged (auto-resolved link)", async () => {
            const conflicts = [
                {
                    skillId: "unchanged-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "link"],
                    contentUnchanged: true,
                    existingSkillMd: "this should not be applied",
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: {
                            succeeded: [{ name: "unchanged-skill", version: "1.0.0" }],
                            failed: [],
                        },
                    },
                ],
            });
            const result = await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            expect(result).not.toBeNull();
            expect(result.linkedSkillVersions.has("unchanged-skill")).toBe(true);
            expect(result.linkedSkillsToReplace.has("unchanged-skill")).toBe(false);
        });
        it("should show 'Linked (existing)' for changed skills the user resolved with 'use existing'", async () => {
            const conflicts = [
                {
                    skillId: "diverged-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    existingSkillMd: "# canonical SKILL.md\n",
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: {
                            succeeded: [{ name: "diverged-skill", version: "1.0.0" }],
                            failed: [],
                        },
                    },
                ],
            });
            vi.mocked(clack.select).mockResolvedValueOnce("link");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            const noteContent = getNoteContent().join("\n");
            expect(noteContent).toContain("Linked");
            expect(noteContent).toContain("diverged-skill");
        });
    });
    describe("batch conflict resolution", () => {
        it("should show batch prompt when multiple unresolved conflicts exist", async () => {
            const conflicts = [
                {
                    skillId: "skill-a",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
                {
                    skillId: "skill-b",
                    exists: true,
                    canPublish: true,
                    latestVersion: "2.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            // Batch prompt: resolve all the same way -> namespace
            vi.mocked(clack.select)
                .mockResolvedValueOnce("all-same") // batch prompt
                .mockResolvedValueOnce("namespace"); // resolution action
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // First select should be the batch prompt
            const firstSelect = vi.mocked(clack.select).mock.calls[0][0];
            const firstOptions = firstSelect.options;
            const optionValues = firstOptions.map((o) => o.value);
            expect(optionValues).toContain("all-same");
            expect(optionValues).toContain("one-by-one");
        });
        it("should NOT show batch prompt when only 1 unresolved conflict exists", async () => {
            const conflicts = [
                {
                    skillId: "single-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            // Direct resolution (no batch prompt)
            vi.mocked(clack.select).mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // Only one select call (the conflict resolution itself)
            expect(clack.select).toHaveBeenCalledTimes(1);
            const selectCall = vi.mocked(clack.select).mock.calls[0][0];
            // Should be the conflict resolution, not the batch prompt
            expect(selectCall.message).toContain("single-skill");
        });
        it("should apply chosen action to all conflicts when 'resolve all same way' is selected with namespace", async () => {
            const conflicts = [
                {
                    skillId: "skill-a",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
                {
                    skillId: "skill-b",
                    exists: true,
                    canPublish: true,
                    latestVersion: "2.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select)
                .mockResolvedValueOnce("all-same")
                .mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // Verify the upload retry used namespace for both
            const lastUploadCall = vi.mocked(callbacks.onUpload).mock.calls[1][0];
            expect(lastUploadCall.resolutionStrategy).toEqual(expect.objectContaining({
                "skill-a": { action: "namespace" },
                "skill-b": { action: "namespace" },
            }));
        });
        it("should apply updateVersion to all with individual incremented versions when 'resolve all same way' is selected", async () => {
            const conflicts = [
                {
                    skillId: "skill-a",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
                {
                    skillId: "skill-b",
                    exists: true,
                    canPublish: true,
                    latestVersion: "2.3.4",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select)
                .mockResolvedValueOnce("all-same")
                .mockResolvedValueOnce("updateVersion");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // Verify each skill got its own incremented version
            const lastUploadCall = vi.mocked(callbacks.onUpload).mock.calls[1][0];
            expect(lastUploadCall.resolutionStrategy).toEqual(expect.objectContaining({
                "skill-a": { action: "updateVersion", version: "1.0.1" },
                "skill-b": { action: "updateVersion", version: "2.3.5" },
            }));
        });
        it("should apply skip (link) to all when 'resolve all same way' is selected with skip", async () => {
            const conflicts = [
                {
                    skillId: "skill-a",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
                {
                    skillId: "skill-b",
                    exists: true,
                    canPublish: true,
                    latestVersion: "2.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select)
                .mockResolvedValueOnce("all-same")
                .mockResolvedValueOnce("link"); // skip
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            const lastUploadCall = vi.mocked(callbacks.onUpload).mock.calls[1][0];
            expect(lastUploadCall.resolutionStrategy).toEqual(expect.objectContaining({
                "skill-a": { action: "link" },
                "skill-b": { action: "link" },
            }));
        });
        it("should fall through to one-by-one resolution when 'choose one-by-one' is selected", async () => {
            const conflicts = [
                {
                    skillId: "skill-a",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
                {
                    skillId: "skill-b",
                    exists: true,
                    canPublish: true,
                    latestVersion: "2.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select)
                .mockResolvedValueOnce("one-by-one") // batch prompt
                .mockResolvedValueOnce("namespace") // skill-a
                .mockResolvedValueOnce("link"); // skill-b (skip)
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // 3 select calls: batch prompt + 2 individual conflicts
            expect(clack.select).toHaveBeenCalledTimes(3);
            const lastUploadCall = vi.mocked(callbacks.onUpload).mock.calls[1][0];
            expect(lastUploadCall.resolutionStrategy).toEqual(expect.objectContaining({
                "skill-a": { action: "namespace" },
                "skill-b": { action: "link" },
            }));
        });
        it("should show note listing all conflicts before batch prompt", async () => {
            const conflicts = [
                {
                    skillId: "skill-a",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
                {
                    skillId: "skill-b",
                    exists: true,
                    canPublish: true,
                    latestVersion: "2.0.0",
                    availableActions: ["cancel", "namespace", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select)
                .mockResolvedValueOnce("all-same")
                .mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // Note should have been called with conflict details
            const noteContent = getNoteContent().join("\n");
            expect(noteContent).toContain("skill-a");
            expect(noteContent).toContain("skill-b");
        });
        it("should only show common actions in batch resolve-all-same-way prompt", async () => {
            const conflicts = [
                {
                    skillId: "skill-a",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
                {
                    skillId: "skill-b",
                    exists: true,
                    canPublish: false,
                    latestVersion: "2.0.0",
                    availableActions: ["cancel", "namespace", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select)
                .mockResolvedValueOnce("all-same")
                .mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // The resolution action select (second call) should only have common actions
            const actionSelect = vi.mocked(clack.select).mock.calls[1][0];
            const actionOptions = actionSelect.options;
            const actionValues = actionOptions.map((o) => o.value);
            // namespace and link (skip) are common, updateVersion is NOT (skill-b can't publish)
            expect(actionValues).toContain("namespace");
            expect(actionValues).toContain("link"); // use existing option
            expect(actionValues).not.toContain("updateVersion");
        });
        it("should handle cancellation during batch prompt", async () => {
            const conflicts = [
                {
                    skillId: "skill-a",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
                {
                    skillId: "skill-b",
                    exists: true,
                    canPublish: true,
                    latestVersion: "2.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [{ success: false, conflicts }],
            });
            // Simulate cancellation
            vi.mocked(clack.isCancel).mockReturnValueOnce(true);
            vi.mocked(clack.select).mockResolvedValueOnce(cancelSymbol);
            const result = await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            expect(result).toBeNull();
        });
        it("should handle mix of auto-resolvable and batch-resolvable conflicts", async () => {
            const conflicts = [
                {
                    skillId: "unchanged-skill",
                    exists: true,
                    canPublish: false,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "link"],
                    contentUnchanged: true, // auto-resolves
                },
                {
                    skillId: "changed-skill-a",
                    exists: true,
                    canPublish: true,
                    latestVersion: "2.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
                {
                    skillId: "changed-skill-b",
                    exists: true,
                    canPublish: true,
                    latestVersion: "3.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            // Batch prompt for the 2 unresolved -> resolve all same way -> namespace
            vi.mocked(clack.select)
                .mockResolvedValueOnce("all-same")
                .mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // Verify combined strategy has auto-resolved + batch-resolved
            const lastUploadCall = vi.mocked(callbacks.onUpload).mock.calls[1][0];
            expect(lastUploadCall.resolutionStrategy).toEqual(expect.objectContaining({
                "unchanged-skill": { action: "link" },
                "changed-skill-a": { action: "namespace" },
                "changed-skill-b": { action: "namespace" },
            }));
        });
    });
    describe("view diff option", () => {
        it("should include View Diff option when conflict has existingSkillMd and content changed", async () => {
            const conflicts = [
                {
                    skillId: "my-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    existingSkillMd: "# Old Content\n\nOld description.",
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            callbacks.onReadLocalSkillMd = vi
                .fn()
                .mockResolvedValue("# New Content\n\nNew description.");
            // User selects namespace directly (skips diff)
            vi.mocked(clack.select).mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            const selectCall = vi.mocked(clack.select).mock.calls[0][0];
            const options = selectCall.options;
            const viewDiffOption = options.find((o) => o.value === "viewDiff");
            expect(viewDiffOption).toBeDefined();
            expect(viewDiffOption?.label).toBe("View Diff");
        });
        it("should NOT include View Diff option when conflict has no existingSkillMd", async () => {
            const conflicts = [
                {
                    skillId: "my-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select).mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            const selectCall = vi.mocked(clack.select).mock.calls[0][0];
            const options = selectCall.options;
            const viewDiffOption = options.find((o) => o.value === "viewDiff");
            expect(viewDiffOption).toBeUndefined();
        });
        it("should display diff via note and re-prompt when user selects View Diff", async () => {
            const conflicts = [
                {
                    skillId: "my-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    existingSkillMd: "Line one\nLine two\nLine three",
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            callbacks.onReadLocalSkillMd = vi
                .fn()
                .mockResolvedValue("Line one\nLine modified\nLine three");
            // First select: user picks "viewDiff", second select: user picks "namespace"
            vi.mocked(clack.select)
                .mockResolvedValueOnce("viewDiff")
                .mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // Should have been called twice (view diff then actual resolution)
            expect(clack.select).toHaveBeenCalledTimes(2);
            // note() should have been called with diff content between the two select calls
            const noteContent = getNoteContent().join("\n");
            expect(noteContent).toContain("Line two");
            expect(noteContent).toContain("Line modified");
            // Should have called the callback to read local content
            expect(callbacks.onReadLocalSkillMd).toHaveBeenCalledWith({
                skillId: "my-skill",
            });
        });
        it("should work in one-by-one mode with multiple conflicts where only some have existingSkillMd", async () => {
            const conflicts = [
                {
                    skillId: "skill-with-diff",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    existingSkillMd: "# Existing",
                },
                {
                    skillId: "skill-without-diff",
                    exists: true,
                    canPublish: true,
                    latestVersion: "2.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            callbacks.onReadLocalSkillMd = vi.fn().mockResolvedValue("# Local");
            // Batch prompt: one-by-one, then resolve each
            vi.mocked(clack.select)
                .mockResolvedValueOnce("one-by-one") // batch
                .mockResolvedValueOnce("namespace") // skill-with-diff (skip viewing diff)
                .mockResolvedValueOnce("namespace"); // skill-without-diff
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // First conflict should have viewDiff option
            const firstConflictCall = vi.mocked(clack.select).mock.calls[1][0];
            const firstOptions = firstConflictCall.options;
            expect(firstOptions.find((o) => o.value === "viewDiff")).toBeDefined();
            // Second conflict should NOT have viewDiff option
            const secondConflictCall = vi.mocked(clack.select).mock.calls[2][0];
            const secondOptions = secondConflictCall.options;
            expect(secondOptions.find((o) => o.value === "viewDiff")).toBeUndefined();
        });
        it("should show full diff without truncation for long content", async () => {
            // Generate a long existing content with many lines
            const longExisting = Array.from({ length: 100 }, (_, i) => `Old line ${i}`).join("\n");
            const longLocal = Array.from({ length: 100 }, (_, i) => `New line ${i}`).join("\n");
            const conflicts = [
                {
                    skillId: "my-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    existingSkillMd: longExisting,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            callbacks.onReadLocalSkillMd = vi.fn().mockResolvedValue(longLocal);
            vi.mocked(clack.select)
                .mockResolvedValueOnce("viewDiff")
                .mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            const noteContent = getNoteContent().join("\n");
            expect(noteContent).not.toContain("truncated");
            // Verify lines beyond the old 50-line limit are present
            expect(noteContent).toContain("Old line 99");
            expect(noteContent).toContain("New line 99");
        });
        it("should handle onReadLocalSkillMd returning null gracefully", async () => {
            const conflicts = [
                {
                    skillId: "my-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    existingSkillMd: "# Existing content",
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            callbacks.onReadLocalSkillMd = vi.fn().mockResolvedValue(null);
            vi.mocked(clack.select)
                .mockResolvedValueOnce("viewDiff")
                .mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // Should still complete without error and show a message about missing file
            expect(clack.select).toHaveBeenCalledTimes(2);
            const noteContent = getNoteContent().join("\n");
            expect(noteContent).toMatch(/not found|could not|unavailable/i);
        });
    });
    describe("inline skills support", () => {
        it("should prompt per-skill and pass resolved inlineSkillIds to onUpload", async () => {
            const callbacks = createMockCallbacks({
                uploadResults: [
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            // User chooses "inline" for the single candidate
            vi.mocked(clack.select).mockResolvedValueOnce("inline");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
                inlineCandidates: ["init"],
            });
            const uploadCall = vi.mocked(callbacks.onUpload).mock.calls[0][0];
            expect(uploadCall.inlineSkillIds).toEqual(["init"]);
        });
        it("should not pass inlineSkillIds when no candidates provided", async () => {
            const callbacks = createMockCallbacks({
                uploadResults: [
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            const uploadCall = vi.mocked(callbacks.onUpload).mock.calls[0][0];
            expect(uploadCall.inlineSkillIds).toBeUndefined();
        });
        it("should prompt each candidate individually when user chooses one-by-one", async () => {
            const callbacks = createMockCallbacks({
                uploadResults: [
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            // Batch choice: one-by-one
            vi.mocked(clack.select)
                .mockResolvedValueOnce("one-by-one") // batch vs one-by-one
                .mockResolvedValueOnce("inline") // init -> inline
                .mockResolvedValueOnce("extract"); // debug -> extract
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
                inlineCandidates: ["init", "debug"],
            });
            const uploadCall = vi.mocked(callbacks.onUpload).mock.calls[0][0];
            // Only "init" was kept inline, "debug" was extracted
            expect(uploadCall.inlineSkillIds).toEqual(["init"]);
        });
        it("should keep all inline when user chooses batch inline", async () => {
            const callbacks = createMockCallbacks({
                uploadResults: [
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            // Batch choice: all-same, then inline
            vi.mocked(clack.select)
                .mockResolvedValueOnce("all-same")
                .mockResolvedValueOnce("inline");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
                inlineCandidates: ["init", "debug"],
            });
            const uploadCall = vi.mocked(callbacks.onUpload).mock.calls[0][0];
            expect(uploadCall.inlineSkillIds).toEqual(["init", "debug"]);
        });
        it("should extract all when user chooses batch extract", async () => {
            const callbacks = createMockCallbacks({
                uploadResults: [
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            // Batch choice: all-same, then extract
            vi.mocked(clack.select)
                .mockResolvedValueOnce("all-same")
                .mockResolvedValueOnce("extract");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
                inlineCandidates: ["init", "debug"],
            });
            const uploadCall = vi.mocked(callbacks.onUpload).mock.calls[0][0];
            // No inline skills when all extracted
            expect(uploadCall.inlineSkillIds).toBeUndefined();
        });
        it("should return null when user cancels inline skill prompt", async () => {
            const callbacks = createMockCallbacks({
                uploadResults: [
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            // User cancels at the inline prompt
            vi.mocked(clack.select).mockResolvedValueOnce(cancelSymbol);
            vi.mocked(clack.isCancel).mockReturnValueOnce(true);
            const result = await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
                inlineCandidates: ["init"],
            });
            expect(result).toBeNull();
            expect(callbacks.onUpload).not.toHaveBeenCalled();
        });
        it("should pass inlineSkillIds on retry with conflict resolution", async () => {
            const conflicts = [
                {
                    skillId: "extracted-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: true,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            // User chooses inline for the candidate
            vi.mocked(clack.select).mockResolvedValueOnce("inline");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
                inlineCandidates: ["init"],
            });
            // Both upload calls should include inlineSkillIds
            const firstUploadCall = vi.mocked(callbacks.onUpload).mock.calls[0][0];
            expect(firstUploadCall.inlineSkillIds).toEqual(["init"]);
            const retryUploadCall = vi.mocked(callbacks.onUpload).mock.calls[1][0];
            expect(retryUploadCall.inlineSkillIds).toEqual(["init"]);
        });
        it("should show inlined skills in summary note", async () => {
            const callbacks = createMockCallbacks({
                uploadResults: [
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: {
                            succeeded: [{ name: "extracted-skill", version: "1.0.0" }],
                            failed: [],
                        },
                    },
                ],
            });
            // User chooses inline for both
            vi.mocked(clack.select)
                .mockResolvedValueOnce("all-same")
                .mockResolvedValueOnce("inline");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
                inlineCandidates: ["init", "debug"],
            });
            const noteContent = getNoteContent().join("\n");
            expect(noteContent).toContain("Inlined");
            expect(noteContent).toContain("init");
            expect(noteContent).toContain("debug");
        });
        it("should include inlineSkillIds in the returned result", async () => {
            const callbacks = createMockCallbacks({
                uploadResults: [
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            // User chooses inline for both
            vi.mocked(clack.select)
                .mockResolvedValueOnce("all-same")
                .mockResolvedValueOnce("inline");
            const result = await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
                inlineCandidates: ["init", "debug"],
            });
            expect(result).not.toBeNull();
            expect(result.inlineSkillIds).toEqual(["init", "debug"]);
        });
        it("should skip prompting in non-interactive mode and extract all", async () => {
            const callbacks = createMockCallbacks({
                uploadResults: [
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            const result = await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
                inlineCandidates: ["init", "debug"],
                nonInteractive: true,
            });
            expect(result).not.toBeNull();
            // No select calls for inline skills in non-interactive mode
            expect(clack.select).not.toHaveBeenCalled();
            const uploadCall = vi.mocked(callbacks.onUpload).mock.calls[0][0];
            expect(uploadCall.inlineSkillIds).toBeUndefined();
        });
    });
    describe("subagent conflict auto-resolution", () => {
        it("should auto-resolve unchanged subagent conflicts and succeed", async () => {
            // Reproduces the lantern/admin scenario: first upload returns unchanged
            // skill conflicts (auto-resolved as link), retry returns unchanged
            // subagent conflicts (auto-resolved as link), final retry succeeds.
            const skillConflicts = [
                {
                    skillId: "using-skills",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.5",
                    availableActions: ["cancel", "namespace", "link", "updateVersion"],
                    contentUnchanged: true,
                },
            ];
            const subagentConflicts = [
                {
                    subagentId: "nori-task-runner",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "link"],
                    contentUnchanged: true,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts: skillConflicts },
                    { success: false, subagentConflicts },
                    {
                        success: true,
                        version: "1.0.8",
                        extractedSkills: { succeeded: [], failed: [] },
                        extractedSubagents: { succeeded: [], failed: [] },
                    },
                ],
            });
            const result = await uploadFlow({
                profileDisplayName: "lantern/admin",
                skillsetName: "admin",
                registryUrl: "https://lantern.noriskillsets.dev",
                callbacks,
            });
            expect(result).not.toBeNull();
            expect(result.version).toBe("1.0.8");
            expect(result.linkedSubagentVersions.get("nori-task-runner")).toBe("1.0.0");
            // No interactive prompts should have been shown for auto-resolvable conflicts
            expect(clack.select).not.toHaveBeenCalled();
            // Some retry must have carried a link-action resolution for the subagent
            const anyRetryLinkedSubagent = vi
                .mocked(callbacks.onUpload)
                .mock.calls.some((call) => call[0].subagentResolutionStrategy?.["nori-task-runner"]?.action ===
                "link");
            expect(anyRetryLinkedSubagent).toBe(true);
        });
    });
    describe("final result diagnostics", () => {
        it("should include the upload result shape in the error log when the result has no known error/conflicts fields", async () => {
            // Regression guard: a future server or client protocol drift could
            // produce an upload result with an unexpected shape. The final handler
            // must surface diagnostic detail instead of a bare "Upload failed".
            const callbacks = createMockCallbacks({
                uploadResults: [{ success: false }],
            });
            const result = await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            expect(result).toBeNull();
            const errorCalls = vi
                .mocked(clack.log.error)
                .mock.calls.map((call) => String(call[0] ?? ""));
            const combined = errorCalls.join("\n");
            expect(combined.toLowerCase()).toContain("unexpected upload result");
        });
    });
    describe("skill conflict fileChanges display", () => {
        it("shows a note listing each changed file before the single-conflict prompt", async () => {
            const conflicts = [
                {
                    skillId: "diverged-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    fileChanges: [
                        { path: "SKILL.md", status: "modified", isBinary: false },
                        { path: "helpers/util.py", status: "added", isBinary: false },
                        { path: "assets/logo.png", status: "modified", isBinary: true },
                    ],
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select).mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            const fileChangeNotes = getFileChangeNotes();
            expect(fileChangeNotes).toHaveLength(1);
            const note = fileChangeNotes[0];
            expect(note.title).toContain("diverged-skill");
            expect(note.body).toContain("SKILL.md");
            expect(note.body).toContain("helpers/util.py");
            expect(note.body).toContain("assets/logo.png");
            expect(note.body).toContain("modified");
            expect(note.body).toContain("added");
        });
        it("does not emit a file-changes note when fileChanges is absent", async () => {
            const conflicts = [
                {
                    skillId: "legacy-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select).mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // No per-conflict "Files changed for ..." note should be emitted.
            expect(getFileChangeNotes()).toHaveLength(0);
        });
        it("does not emit a file-changes note when fileChanges is empty", async () => {
            const conflicts = [
                {
                    skillId: "empty-changes-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    fileChanges: [],
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select).mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            expect(getFileChangeNotes()).toHaveLength(0);
        });
        it("uses pluralized file-change count in the Use Existing hint when fileChanges is non-empty", async () => {
            const conflicts = [
                {
                    skillId: "multi-change",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    fileChanges: [
                        { path: "SKILL.md", status: "modified", isBinary: false },
                        { path: "helpers/a.py", status: "modified", isBinary: false },
                        { path: "helpers/b.py", status: "added", isBinary: false },
                    ],
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select).mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // Look at the select call whose prompt mentions the conflict skillId.
            const conflictCall = vi
                .mocked(clack.select)
                .mock.calls.find((call) => String(call[0].message ?? "").includes("multi-change"));
            expect(conflictCall).toBeDefined();
            const options = conflictCall[0].options;
            const useExistingOption = options.find((o) => o.value === "link");
            expect(useExistingOption).toBeDefined();
            expect(useExistingOption?.hint).toContain("3 file changes");
        });
        it("uses singular form '1 file change' in Use Existing hint when exactly one file changed", async () => {
            const conflicts = [
                {
                    skillId: "single-change",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    fileChanges: [
                        { path: "SKILL.md", status: "modified", isBinary: false },
                    ],
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select).mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            const conflictCall = vi
                .mocked(clack.select)
                .mock.calls.find((call) => String(call[0].message ?? "").includes("single-change"));
            expect(conflictCall).toBeDefined();
            const options = conflictCall[0].options;
            const useExistingOption = options.find((o) => o.value === "link");
            expect(useExistingOption?.hint).toContain("1 file change");
            expect(useExistingOption?.hint).not.toContain("1 file changes");
        });
        it("falls back to the default discard hint when fileChanges is not provided", async () => {
            const conflicts = [
                {
                    skillId: "legacy-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select).mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            const options = vi.mocked(clack.select).mock.calls[0][0]
                .options;
            const useExistingOption = options.find((o) => o.value === "link");
            expect(useExistingOption?.hint).toContain("discard any local changes");
        });
        it("falls back to the generic discard hint in batch mode when some conflicts lack fileChanges", async () => {
            const conflicts = [
                {
                    skillId: "new-style",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    fileChanges: [
                        { path: "SKILL.md", status: "modified", isBinary: false },
                    ],
                },
                {
                    skillId: "old-style",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    // no fileChanges — older registrar, or skill without the data
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select)
                .mockResolvedValueOnce("all-same")
                .mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            const batchActionCall = vi
                .mocked(clack.select)
                .mock.calls.find((call) => String(call[0].message ?? "").includes("How should all conflicts be resolved"));
            expect(batchActionCall).toBeDefined();
            const options = batchActionCall[0].options;
            const useExistingOption = options.find((o) => o.value === "link");
            // Under-reporting would say "1 file change" even though old-style's
            // impact is unknown. Fall back to the generic clause instead.
            expect(useExistingOption?.hint).toContain("discard any local changes");
            expect(useExistingOption?.hint).not.toContain("1 file change");
        });
        it("sums file-change counts across conflicts in the batch 'resolve all same way' Use Existing hint", async () => {
            const conflicts = [
                {
                    skillId: "skill-a",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    fileChanges: [
                        { path: "SKILL.md", status: "modified", isBinary: false },
                        { path: "a.py", status: "added", isBinary: false },
                    ],
                },
                {
                    skillId: "skill-b",
                    exists: true,
                    canPublish: true,
                    latestVersion: "2.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    fileChanges: [
                        { path: "SKILL.md", status: "modified", isBinary: false },
                    ],
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, conflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            // The flow emits a "how would you like to resolve" batch select,
            // then the batch-action select within resolveAllConflictsSameWay.
            vi.mocked(clack.select)
                .mockResolvedValueOnce("all-same")
                .mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            // Locate the batch-action select by its message to avoid depending on
            // the exact ordering of earlier select calls.
            const batchActionCall = vi
                .mocked(clack.select)
                .mock.calls.find((call) => String(call[0].message ?? "").includes("How should all conflicts be resolved"));
            expect(batchActionCall).toBeDefined();
            const options = batchActionCall[0].options;
            const useExistingOption = options.find((o) => o.value === "link");
            expect(useExistingOption).toBeDefined();
            expect(useExistingOption?.hint).toContain("3 file changes");
        });
    });
    describe("subagent conflict fileChanges display", () => {
        it("shows a note listing each changed file for a subagent conflict", async () => {
            const subagentConflicts = [
                {
                    subagentId: "my-subagent",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    fileChanges: [
                        { path: "SUBAGENT.md", status: "modified", isBinary: false },
                        { path: "prompts/intro.md", status: "added", isBinary: false },
                    ],
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, subagentConflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select).mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            const fileChangeNotes = getFileChangeNotes();
            expect(fileChangeNotes).toHaveLength(1);
            const note = fileChangeNotes[0];
            expect(note.title).toContain("my-subagent");
            expect(note.body).toContain("SUBAGENT.md");
            expect(note.body).toContain("prompts/intro.md");
            expect(note.body).toContain("modified");
            expect(note.body).toContain("added");
        });
        it("uses pluralized file-change count in the subagent Use Existing hint", async () => {
            const subagentConflicts = [
                {
                    subagentId: "multi-change-subagent",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    fileChanges: [
                        { path: "SUBAGENT.md", status: "modified", isBinary: false },
                        { path: "prompts/intro.md", status: "added", isBinary: false },
                    ],
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [
                    { success: false, subagentConflicts },
                    {
                        success: true,
                        version: "1.0.0",
                        extractedSkills: { succeeded: [], failed: [] },
                    },
                ],
            });
            vi.mocked(clack.select).mockResolvedValueOnce("namespace");
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
            });
            const conflictCall = vi
                .mocked(clack.select)
                .mock.calls.find((call) => String(call[0].message ?? "").includes("multi-change-subagent"));
            expect(conflictCall).toBeDefined();
            const options = conflictCall[0].options;
            const useExistingOption = options.find((o) => o.value === "link");
            expect(useExistingOption?.hint).toContain("2 file changes");
        });
    });
    describe("non-interactive conflict output with fileChanges", () => {
        it("includes a compact per-conflict file-change summary in the non-interactive error note", async () => {
            const conflicts = [
                {
                    skillId: "diverged-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                    fileChanges: [
                        { path: "SKILL.md", status: "modified", isBinary: false },
                        { path: "a.py", status: "added", isBinary: false },
                        { path: "b.py", status: "added", isBinary: false },
                    ],
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [{ success: false, conflicts }],
            });
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
                nonInteractive: true,
            });
            const noteContent = getNoteContent().join("\n");
            expect(noteContent).toContain("diverged-skill");
            // Summary-only: tally not full paths. Specifically does NOT list
            // individual file paths in non-interactive mode to keep output compact.
            expect(noteContent).toContain("2 added");
            expect(noteContent).toContain("1 modified");
            expect(noteContent).not.toContain("a.py");
        });
        it("omits the per-conflict file-change summary when fileChanges is absent", async () => {
            const conflicts = [
                {
                    skillId: "legacy-skill",
                    exists: true,
                    canPublish: true,
                    latestVersion: "1.0.0",
                    availableActions: ["cancel", "namespace", "updateVersion", "link"],
                    contentUnchanged: false,
                },
            ];
            const callbacks = createMockCallbacks({
                uploadResults: [{ success: false, conflicts }],
            });
            await uploadFlow({
                profileDisplayName: "myorg/my-profile",
                skillsetName: "my-profile",
                registryUrl: "https://myorg.noriskillsets.dev",
                callbacks,
                nonInteractive: true,
            });
            const noteContent = getNoteContent().join("\n");
            expect(noteContent).toContain("legacy-skill");
            // No "N added" / "N modified" tally should appear.
            expect(noteContent).not.toMatch(/\d+ added/);
            expect(noteContent).not.toMatch(/\d+ modified/);
            expect(noteContent).not.toMatch(/\d+ removed/);
        });
    });
});
//# sourceMappingURL=upload.test.js.map