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

import type { GeminiCLIExtension } from '@google/gemini-cli-core';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import { MessageType } from '../types.js';
import { extensionsCommand } from './extensionsCommand.js';
import { type CommandContext } from './types.js';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { type ExtensionUpdateAction } from '../state/extensions.js';

vi.mock('../../config/extensions/update.js', () => ({
  updateExtension: vi.fn(),
  checkForAllExtensionUpdates: vi.fn(),
}));

const mockGetExtensions = vi.fn();

describe('extensionsCommand', () => {
  let mockContext: CommandContext;
  const mockDispatchExtensionState = vi.fn();

  beforeEach(() => {
    vi.resetAllMocks();
    mockGetExtensions.mockReturnValue([]);
    mockContext = createMockCommandContext({
      services: {
        config: {
          getExtensions: mockGetExtensions,
          getWorkingDir: () => '/test/dir',
        },
      },
      ui: {
        dispatchExtensionStateUpdate: mockDispatchExtensionState,
      },
    });
  });

  describe('list', () => {
    it('should add an EXTENSIONS_LIST item to the UI', async () => {
      if (!extensionsCommand.action) throw new Error('Action not defined');
      await extensionsCommand.action(mockContext, '');

      expect(mockContext.ui.addItem).toHaveBeenCalledWith(
        {
          type: MessageType.EXTENSIONS_LIST,
          extensions: expect.any(Array),
        },
        expect.any(Number),
      );
    });
  });

  describe('update', () => {
    const updateAction = extensionsCommand.subCommands?.find(
      (cmd) => cmd.name === 'update',
    )?.action;

    if (!updateAction) {
      throw new Error('Update action not found');
    }

    it('should show usage if no args are provided', async () => {
      await updateAction(mockContext, '');
      expect(mockContext.ui.addItem).toHaveBeenCalledWith(
        {
          type: MessageType.ERROR,
          text: 'Usage: /extensions update <extension-names>|--all',
        },
        expect.any(Number),
      );
    });

    it('should inform user if there are no extensions to update with --all', async () => {
      mockDispatchExtensionState.mockImplementationOnce(
        (action: ExtensionUpdateAction) => {
          if (action.type === 'SCHEDULE_UPDATE') {
            action.payload.onComplete([]);
          }
        },
      );

      await updateAction(mockContext, '--all');
      expect(mockContext.ui.addItem).toHaveBeenCalledWith(
        {
          type: MessageType.INFO,
          text: 'No extensions to update.',
        },
        expect.any(Number),
      );
    });

    it('should call setPendingItem and addItem in a finally block on success', async () => {
      mockDispatchExtensionState.mockImplementationOnce(
        (action: ExtensionUpdateAction) => {
          if (action.type === 'SCHEDULE_UPDATE') {
            action.payload.onComplete([
              {
                name: 'ext-one',
                originalVersion: '1.0.0',
                updatedVersion: '1.0.1',
              },
              {
                name: 'ext-two',
                originalVersion: '2.0.0',
                updatedVersion: '2.0.1',
              },
            ]);
          }
        },
      );
      await updateAction(mockContext, '--all');
      expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith({
        type: MessageType.EXTENSIONS_LIST,
        extensions: expect.any(Array),
      });
      expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith(null);
      expect(mockContext.ui.addItem).toHaveBeenCalledWith(
        {
          type: MessageType.EXTENSIONS_LIST,
          extensions: expect.any(Array),
        },
        expect.any(Number),
      );
    });

    it('should call setPendingItem and addItem in a finally block on failure', async () => {
      mockDispatchExtensionState.mockImplementationOnce((_) => {
        throw new Error('Something went wrong');
      });
      await updateAction(mockContext, '--all');
      expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith({
        type: MessageType.EXTENSIONS_LIST,
        extensions: expect.any(Array),
      });
      expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith(null);
      expect(mockContext.ui.addItem).toHaveBeenCalledWith(
        {
          type: MessageType.EXTENSIONS_LIST,
          extensions: expect.any(Array),
        },
        expect.any(Number),
      );
      expect(mockContext.ui.addItem).toHaveBeenCalledWith(
        {
          type: MessageType.ERROR,
          text: 'Something went wrong',
        },
        expect.any(Number),
      );
    });

    it('should update a single extension by name', async () => {
      mockDispatchExtensionState.mockImplementationOnce(
        (action: ExtensionUpdateAction) => {
          if (action.type === 'SCHEDULE_UPDATE') {
            action.payload.onComplete([
              {
                name: 'ext-one',
                originalVersion: '1.0.0',
                updatedVersion: '1.0.1',
              },
            ]);
          }
        },
      );
      await updateAction(mockContext, 'ext-one');
      expect(mockDispatchExtensionState).toHaveBeenCalledWith({
        type: 'SCHEDULE_UPDATE',
        payload: {
          all: false,
          names: ['ext-one'],
          onComplete: expect.any(Function),
        },
      });
    });

    it('should update multiple extensions by name', async () => {
      mockDispatchExtensionState.mockImplementationOnce(
        (action: ExtensionUpdateAction) => {
          if (action.type === 'SCHEDULE_UPDATE') {
            action.payload.onComplete([
              {
                name: 'ext-one',
                originalVersion: '1.0.0',
                updatedVersion: '1.0.1',
              },
              {
                name: 'ext-two',
                originalVersion: '1.0.0',
                updatedVersion: '1.0.1',
              },
            ]);
          }
        },
      );
      await updateAction(mockContext, 'ext-one ext-two');
      expect(mockDispatchExtensionState).toHaveBeenCalledWith({
        type: 'SCHEDULE_UPDATE',
        payload: {
          all: false,
          names: ['ext-one', 'ext-two'],
          onComplete: expect.any(Function),
        },
      });
      expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith({
        type: MessageType.EXTENSIONS_LIST,
        extensions: expect.any(Array),
      });
      expect(mockContext.ui.setPendingItem).toHaveBeenCalledWith(null);
      expect(mockContext.ui.addItem).toHaveBeenCalledWith(
        {
          type: MessageType.EXTENSIONS_LIST,
          extensions: expect.any(Array),
        },
        expect.any(Number),
      );
    });

    describe('completion', () => {
      const updateCompletion = extensionsCommand.subCommands?.find(
        (cmd) => cmd.name === 'update',
      )?.completion;

      if (!updateCompletion) {
        throw new Error('Update completion not found');
      }

      const extensionOne: GeminiCLIExtension = {
        name: 'ext-one',
        id: 'ext-one-id',
        version: '1.0.0',
        isActive: true,
        path: '/test/dir/ext-one',
        contextFiles: [],
        installMetadata: {
          type: 'git',
          autoUpdate: false,
          source: 'https://github.com/some/extension.git',
        },
      };
      const extensionTwo: GeminiCLIExtension = {
        name: 'another-ext',
        id: 'another-ext-id',
        version: '1.0.0',
        isActive: true,
        path: '/test/dir/another-ext',
        contextFiles: [],
        installMetadata: {
          type: 'git',
          autoUpdate: false,
          source: 'https://github.com/some/extension.git',
        },
      };
      const allExt: GeminiCLIExtension = {
        name: 'all-ext',
        id: 'all-ext-id',
        version: '1.0.0',
        isActive: true,
        path: '/test/dir/all-ext',
        contextFiles: [],
        installMetadata: {
          type: 'git',
          autoUpdate: false,
          source: 'https://github.com/some/extension.git',
        },
      };

      it.each([
        {
          description: 'should return matching extension names',
          extensions: [extensionOne, extensionTwo],
          partialArg: 'ext',
          expected: ['ext-one'],
        },
        {
          description: 'should return --all when partialArg matches',
          extensions: [],
          partialArg: '--al',
          expected: ['--all'],
        },
        {
          description:
            'should return both extension names and --all when both match',
          extensions: [allExt],
          partialArg: 'all',
          expected: ['--all', 'all-ext'],
        },
        {
          description: 'should return an empty array if no matches',
          extensions: [extensionOne],
          partialArg: 'nomatch',
          expected: [],
        },
      ])('$description', async ({ extensions, partialArg, expected }) => {
        mockGetExtensions.mockReturnValue(extensions);
        const suggestions = await updateCompletion(mockContext, partialArg);
        expect(suggestions).toEqual(expected);
      });
    });
  });
});
