/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { skillsCommand } from './skillsCommand.js';
import { MessageType, type HistoryItemSkillsList } from '../types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import type { CommandContext } from './types.js';
import type { Config, SkillDefinition } from '@google/gemini-cli-core';
import {
  SettingScope,
  type LoadedSettings,
  createTestMergedSettings,
  type MergedSettings,
} from '../../config/settings.js';

vi.mock('../../utils/skillUtils.js', async (importOriginal) => {
  const actual =
    await importOriginal<typeof import('../../utils/skillUtils.js')>();
  return {
    ...actual,
    linkSkill: vi.fn(),
  };
});

vi.mock('../../config/extensions/consent.js', async (importOriginal) => {
  const actual =
    await importOriginal<typeof import('../../config/extensions/consent.js')>();
  return {
    ...actual,
    requestConsentInteractive: vi.fn().mockResolvedValue(true),
    skillsConsentString: vi.fn().mockResolvedValue('Mock Consent'),
  };
});

import { linkSkill } from '../../utils/skillUtils.js';
import { requestConsentInteractive } from '../../config/extensions/consent.js';

vi.mock('../../config/settings.js', async (importOriginal) => {
  const actual =
    await importOriginal<typeof import('../../config/settings.js')>();
  return {
    ...actual,
    isLoadableSettingScope: vi.fn((s) => s === 'User' || s === 'Workspace'),
  };
});

describe('skillsCommand', () => {
  let context: CommandContext;

  beforeEach(() => {
    vi.useFakeTimers();
    const skills = [
      {
        name: 'skill1',
        description: 'desc1',
        location: '/loc1',
        body: 'body1',
      },
      {
        name: 'skill2',
        description: 'desc2',
        location: '/loc2',
        body: 'body2',
      },
    ];
    context = createMockCommandContext({
      services: {
        agentContext: {
          getSkillManager: vi.fn().mockReturnValue({
            getAllSkills: vi.fn().mockReturnValue(skills),
            getSkills: vi.fn().mockReturnValue(skills),
            isAdminEnabled: vi.fn().mockReturnValue(true),
            getSkill: vi
              .fn()
              .mockImplementation(
                (name: string) => skills.find((s) => s.name === name) ?? null,
              ),
          }),
          getContentGenerator: vi.fn(),
          get config() {
            return this;
          },
        } as unknown as Config,
        settings: {
          merged: createTestMergedSettings({ skills: { disabled: [] } }),
          workspace: { path: '/workspace' },
          setValue: vi.fn(),
        } as unknown as LoadedSettings,
      },
    });
  });

  afterEach(() => {
    vi.useRealTimers();
    vi.restoreAllMocks();
  });

  it('should add a SKILLS_LIST item to UI with descriptions by default', async () => {
    await skillsCommand.action!(context, '');

    expect(context.ui.addItem).toHaveBeenCalledWith(
      expect.objectContaining({
        type: MessageType.SKILLS_LIST,
        skills: [
          {
            name: 'skill1',
            description: 'desc1',
            disabled: undefined,
            location: '/loc1',
            body: 'body1',
          },
          {
            name: 'skill2',
            description: 'desc2',
            disabled: undefined,
            location: '/loc2',
            body: 'body2',
          },
        ],
        showDescriptions: true,
      }),
    );
  });

  it('should list skills when "list" subcommand is used', async () => {
    const listCmd = skillsCommand.subCommands!.find((s) => s.name === 'list')!;
    await listCmd.action!(context, '');

    expect(context.ui.addItem).toHaveBeenCalledWith(
      expect.objectContaining({
        type: MessageType.SKILLS_LIST,
        skills: [
          {
            name: 'skill1',
            description: 'desc1',
            disabled: undefined,
            location: '/loc1',
            body: 'body1',
          },
          {
            name: 'skill2',
            description: 'desc2',
            disabled: undefined,
            location: '/loc2',
            body: 'body2',
          },
        ],
        showDescriptions: true,
      }),
    );
  });

  it('should disable descriptions if "nodesc" arg is provided to list', async () => {
    const listCmd = skillsCommand.subCommands!.find((s) => s.name === 'list')!;
    await listCmd.action!(context, 'nodesc');

    expect(context.ui.addItem).toHaveBeenCalledWith(
      expect.objectContaining({
        showDescriptions: false,
      }),
    );
  });

  it('should filter built-in skills by default and show them with "all"', async () => {
    const skillManager =
      context.services.agentContext!.config.getSkillManager();
    const mockSkills = [
      {
        name: 'regular',
        description: 'desc1',
        location: '/loc1',
        body: 'body1',
      },
      {
        name: 'builtin',
        description: 'desc2',
        location: '/loc2',
        body: 'body2',
        isBuiltin: true,
      },
    ];
    vi.mocked(skillManager.getAllSkills).mockReturnValue(mockSkills);

    const listCmd = skillsCommand.subCommands!.find((s) => s.name === 'list')!;

    // By default, only regular skills
    await listCmd.action!(context, '');
    let lastCall = vi
      .mocked(context.ui.addItem)
      .mock.calls.at(-1)![0] as HistoryItemSkillsList;
    expect(lastCall.skills).toHaveLength(1);
    expect(lastCall.skills[0].name).toBe('regular');

    // With "all", show both
    await listCmd.action!(context, 'all');
    lastCall = vi
      .mocked(context.ui.addItem)
      .mock.calls.at(-1)![0] as HistoryItemSkillsList;
    expect(lastCall.skills).toHaveLength(2);
    expect(lastCall.skills.map((s) => s.name)).toContain('builtin');

    // With "--all", show both
    await listCmd.action!(context, '--all');
    lastCall = vi
      .mocked(context.ui.addItem)
      .mock.calls.at(-1)![0] as HistoryItemSkillsList;
    expect(lastCall.skills).toHaveLength(2);
  });

  describe('link', () => {
    it('should link a skill successfully', async () => {
      const linkCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'link',
      )!;
      vi.mocked(linkSkill).mockResolvedValue([
        { name: 'test-skill', location: '/path' },
      ]);

      await linkCmd.action!(context, '/some/path');

      expect(linkSkill).toHaveBeenCalledWith(
        '/some/path',
        'user',
        expect.any(Function),
        expect.any(Function),
      );
      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.INFO,
          text: 'Successfully linked skills from "/some/path" (user).',
        }),
      );
    });

    it('should link a skill with workspace scope', async () => {
      const linkCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'link',
      )!;
      vi.mocked(linkSkill).mockResolvedValue([
        { name: 'test-skill', location: '/path' },
      ]);

      await linkCmd.action!(context, '/some/path --scope workspace');

      expect(linkSkill).toHaveBeenCalledWith(
        '/some/path',
        'workspace',
        expect.any(Function),
        expect.any(Function),
      );
    });

    it('should pass a cleanup callback for interactive workspace consent', async () => {
      const linkCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'link',
      )!;
      context.ui.setConfirmationRequest = vi.fn();
      vi.mocked(linkSkill).mockImplementation(
        async (_sourcePath, _scope, _addItem, requestConsent) => {
          expect(requestConsent).toBeDefined();
          await requestConsent!(
            [{ name: 'test-skill', location: '/path' } as SkillDefinition],
            '/workspace/.gemini/skills',
          );
          return [{ name: 'test-skill', location: '/path' }];
        },
      );

      await linkCmd.action!(context, '/some/path --scope workspace');

      const requestConsentCall = vi
        .mocked(requestConsentInteractive)
        .mock.calls.at(-1);
      expect(requestConsentCall?.[1]).toEqual(expect.any(Function));

      const clearConfirmationRequest = requestConsentCall?.[2];
      expect(clearConfirmationRequest).toBeTypeOf('function');

      clearConfirmationRequest?.();
      expect(context.ui.setConfirmationRequest).toHaveBeenCalledWith(null);
    });

    it('should show error if link fails', async () => {
      const linkCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'link',
      )!;
      vi.mocked(linkSkill).mockRejectedValue(new Error('Link failed'));

      await linkCmd.action!(context, '/some/path');

      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.ERROR,
          text: 'Failed to link skills: Link failed',
        }),
      );
    });

    it('should show error if path is missing', async () => {
      const linkCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'link',
      )!;
      await linkCmd.action!(context, '');

      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.ERROR,
          text: 'Usage: /skills link <path> [--scope user|workspace]',
        }),
      );
    });
  });

  describe('disable/enable', () => {
    beforeEach(() => {
      (
        context.services.settings as unknown as { merged: MergedSettings }
      ).merged = createTestMergedSettings({
        skills: { enabled: true, disabled: [] },
      });
      (
        context.services.settings as unknown as { workspace: { path: string } }
      ).workspace = {
        path: '/workspace',
      };

      interface MockSettings {
        user: { settings: unknown; path: string };
        workspace: { settings: unknown; path: string };
        forScope: unknown;
      }

      const settings = context.services.settings as unknown as MockSettings;

      settings.forScope = vi.fn((scope) => {
        if (scope === SettingScope.User) return settings.user;
        if (scope === SettingScope.Workspace) return settings.workspace;
        return { settings: {}, path: '' };
      });
      settings.user = {
        settings: {},
        path: '/user/settings.json',
      };
      settings.workspace = {
        settings: {},
        path: '/workspace',
      };
    });

    it('should disable a skill', async () => {
      const disableCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'disable',
      )!;
      await disableCmd.action!(context, 'skill1');

      expect(context.services.settings.setValue).toHaveBeenCalledWith(
        SettingScope.Workspace,
        'skills.disabled',
        ['skill1'],
      );
      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.INFO,
          text: 'Skill "skill1" disabled by adding it to the disabled list in workspace (/workspace) settings. You can run "/skills reload" to refresh your current instance.',
        }),
      );
    });

    it('should show reload guidance even if skill is already disabled', async () => {
      const disableCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'disable',
      )!;
      (
        context.services.settings as unknown as { merged: MergedSettings }
      ).merged = createTestMergedSettings({
        skills: { enabled: true, disabled: ['skill1'] },
      });
      (
        context.services.settings as unknown as {
          workspace: { settings: { skills: { disabled: string[] } } };
        }
      ).workspace.settings = {
        skills: { disabled: ['skill1'] },
      };

      await disableCmd.action!(context, 'skill1');

      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.INFO,
          text: 'Skill "skill1" is already disabled. You can run "/skills reload" to refresh your current instance.',
        }),
      );
    });

    it('should enable a skill', async () => {
      const enableCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'enable',
      )!;
      (
        context.services.settings as unknown as { merged: MergedSettings }
      ).merged = createTestMergedSettings({
        skills: {
          enabled: true,
          disabled: ['skill1'],
        },
      });
      (
        context.services.settings as unknown as {
          workspace: { settings: { skills: { disabled: string[] } } };
        }
      ).workspace.settings = {
        skills: { disabled: ['skill1'] },
      };

      await enableCmd.action!(context, 'skill1');

      expect(context.services.settings.setValue).toHaveBeenCalledWith(
        SettingScope.Workspace,
        'skills.disabled',
        [],
      );
      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.INFO,
          text: 'Skill "skill1" enabled by removing it from the disabled list in workspace (/workspace) and user (/user/settings.json) settings. You can run "/skills reload" to refresh your current instance.',
        }),
      );
    });

    it('should enable a skill across multiple scopes', async () => {
      const enableCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'enable',
      )!;
      (
        context.services.settings as unknown as {
          user: { settings: { skills: { disabled: string[] } } };
        }
      ).user.settings = {
        skills: { disabled: ['skill1'] },
      };
      (
        context.services.settings as unknown as {
          workspace: { settings: { skills: { disabled: string[] } } };
        }
      ).workspace.settings = {
        skills: { disabled: ['skill1'] },
      };

      await enableCmd.action!(context, 'skill1');

      expect(context.services.settings.setValue).toHaveBeenCalledWith(
        SettingScope.User,
        'skills.disabled',
        [],
      );
      expect(context.services.settings.setValue).toHaveBeenCalledWith(
        SettingScope.Workspace,
        'skills.disabled',
        [],
      );
      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.INFO,
          text: 'Skill "skill1" enabled by removing it from the disabled list in workspace (/workspace) and user (/user/settings.json) settings. You can run "/skills reload" to refresh your current instance.',
        }),
      );
    });

    it('should show error if skill not found during disable', async () => {
      const disableCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'disable',
      )!;
      await disableCmd.action!(context, 'non-existent');

      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.ERROR,
          text: 'Skill "non-existent" not found.',
        }),
        expect.any(Number),
      );
    });

    it('should show error if skills are disabled by admin during disable', async () => {
      const skillManager =
        context.services.agentContext!.config.getSkillManager();
      vi.mocked(skillManager.isAdminEnabled).mockReturnValue(false);

      const disableCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'disable',
      )!;
      await disableCmd.action!(context, 'skill1');

      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.ERROR,
          text: 'Agent skills is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
        }),
        expect.any(Number),
      );
    });

    it('should show error if skills are disabled by admin during enable', async () => {
      const skillManager =
        context.services.agentContext!.config.getSkillManager();
      vi.mocked(skillManager.isAdminEnabled).mockReturnValue(false);

      const enableCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'enable',
      )!;
      await enableCmd.action!(context, 'skill1');

      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.ERROR,
          text: 'Agent skills is disabled by your administrator. To enable it, please request an update to the settings at: https://goo.gle/manage-gemini-cli',
        }),
        expect.any(Number),
      );
    });
  });

  describe('reload', () => {
    it('should reload skills successfully and show success message', async () => {
      const reloadCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'reload',
      )!;
      // Make reload take some time so timer can fire
      const reloadSkillsMock = vi.fn().mockImplementation(async () => {
        await new Promise((resolve) => setTimeout(resolve, 200));
      });
      context.services.agentContext!.config.reloadSkills = reloadSkillsMock;

      const actionPromise = reloadCmd.action!(context, '');

      // Initially, no pending item (flicker prevention)
      expect(context.ui.setPendingItem).not.toHaveBeenCalled();

      // Fast forward 100ms to trigger the pending item
      await vi.advanceTimersByTimeAsync(100);
      expect(context.ui.setPendingItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.INFO,
          text: 'Reloading agent skills...',
        }),
      );

      // Fast forward another 100ms (reload complete), but pending item should stay
      await vi.advanceTimersByTimeAsync(100);
      expect(context.ui.setPendingItem).not.toHaveBeenCalledWith(null);

      // Fast forward to reach 500ms total
      await vi.advanceTimersByTimeAsync(300);
      await actionPromise;

      expect(reloadSkillsMock).toHaveBeenCalled();
      expect(context.ui.reloadCommands).toHaveBeenCalled();
      expect(context.ui.setPendingItem).toHaveBeenCalledWith(null);
      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.INFO,
          text: 'Agent skills reloaded successfully.',
        }),
      );
    });

    it('should show new skills count after reload', async () => {
      const reloadCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'reload',
      )!;
      const reloadSkillsMock = vi.fn().mockImplementation(async () => {
        const skillManager =
          context.services.agentContext!.config.getSkillManager();
        vi.mocked(skillManager.getSkills).mockReturnValue([
          { name: 'skill1' },
          { name: 'skill2' },
          { name: 'skill3' },
        ] as SkillDefinition[]);
      });
      context.services.agentContext!.config.reloadSkills = reloadSkillsMock;

      await reloadCmd.action!(context, '');

      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.INFO,
          text: 'Agent skills reloaded successfully. 1 newly available skill.',
        }),
      );
    });

    it('should show removed skills count after reload', async () => {
      const reloadCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'reload',
      )!;
      const reloadSkillsMock = vi.fn().mockImplementation(async () => {
        const skillManager =
          context.services.agentContext!.config.getSkillManager();
        vi.mocked(skillManager.getSkills).mockReturnValue([
          { name: 'skill1' },
        ] as SkillDefinition[]);
      });
      context.services.agentContext!.config.reloadSkills = reloadSkillsMock;

      await reloadCmd.action!(context, '');

      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.INFO,
          text: 'Agent skills reloaded successfully. 1 skill no longer available.',
        }),
      );
    });

    it('should show both added and removed skills count after reload', async () => {
      const reloadCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'reload',
      )!;
      const reloadSkillsMock = vi.fn().mockImplementation(async () => {
        const skillManager =
          context.services.agentContext!.config.getSkillManager();
        vi.mocked(skillManager.getSkills).mockReturnValue([
          { name: 'skill2' }, // skill1 removed, skill3 added
          { name: 'skill3' },
        ] as SkillDefinition[]);
      });
      context.services.agentContext!.config.reloadSkills = reloadSkillsMock;

      await reloadCmd.action!(context, '');

      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.INFO,
          text: 'Agent skills reloaded successfully. 1 newly available skill and 1 skill no longer available.',
        }),
      );
    });

    it('should show error if configuration is missing', async () => {
      const reloadCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'reload',
      )!;
      context.services.agentContext = null;

      await reloadCmd.action!(context, '');

      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.ERROR,
          text: 'Could not retrieve configuration.',
        }),
      );
    });

    it('should show error if reload fails', async () => {
      const reloadCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'reload',
      )!;
      const error = new Error('Reload failed');
      const reloadSkillsMock = vi.fn().mockImplementation(async () => {
        await new Promise((_, reject) => setTimeout(() => reject(error), 200));
      });
      context.services.agentContext!.config.reloadSkills = reloadSkillsMock;

      const actionPromise = reloadCmd.action!(context, '');
      await vi.advanceTimersByTimeAsync(100);
      await vi.advanceTimersByTimeAsync(400);
      await actionPromise;

      expect(context.ui.setPendingItem).toHaveBeenCalledWith(null);
      expect(context.ui.addItem).toHaveBeenCalledWith(
        expect.objectContaining({
          type: MessageType.ERROR,
          text: 'Failed to reload skills: Reload failed',
        }),
      );
    });
  });

  describe('completions', () => {
    it('should provide completions for disable (only enabled skills)', async () => {
      const disableCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'disable',
      )!;
      const skillManager =
        context.services.agentContext!.config.getSkillManager();
      const mockSkills = [
        {
          name: 'skill1',
          description: 'desc1',
          disabled: false,
          location: '/loc1',
          body: 'body1',
        },
        {
          name: 'skill2',
          description: 'desc2',
          disabled: true,
          location: '/loc2',
          body: 'body2',
        },
      ];
      vi.mocked(skillManager.getAllSkills).mockReturnValue(mockSkills);
      vi.mocked(skillManager.getSkill).mockImplementation(
        (name: string) => mockSkills.find((s) => s.name === name) ?? null,
      );

      const completions = await disableCmd.completion!(context, 'sk');
      expect(completions).toEqual(['skill1']);
    });

    it('should provide completions for enable (only disabled skills)', async () => {
      const enableCmd = skillsCommand.subCommands!.find(
        (s) => s.name === 'enable',
      )!;
      const skillManager =
        context.services.agentContext!.config.getSkillManager();
      const mockSkills = [
        {
          name: 'skill1',
          description: 'desc1',
          disabled: false,
          location: '/loc1',
          body: 'body1',
        },
        {
          name: 'skill2',
          description: 'desc2',
          disabled: true,
          location: '/loc2',
          body: 'body2',
        },
      ];
      vi.mocked(skillManager.getAllSkills).mockReturnValue(mockSkills);
      vi.mocked(skillManager.getSkill).mockImplementation(
        (name: string) => mockSkills.find((s) => s.name === name) ?? null,
      );

      const completions = await enableCmd.completion!(context, 'sk');
      expect(completions).toEqual(['skill2']);
    });
  });
});
