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

import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';

vi.mock('node:child_process', async (importOriginal) => {
  const actual = await importOriginal();
  return {
    ...(actual as object),
    execSync: vi.fn(),
    spawnSync: vi.fn(() => ({ status: 0 })),
  };
});
vi.mock('node:fs');
vi.mock('node:os');
vi.mock('../utils/paths.js', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../utils/paths.js')>();
  return {
    ...actual,
    homedir: vi.fn(),
  };
});

import { getIdeInstaller } from './ide-installer.js';
import * as child_process from 'node:child_process';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { IDE_DEFINITIONS, type IdeInfo } from './detect-ide.js';
import { homedir as pathsHomedir } from '../utils/paths.js';

describe('ide-installer', () => {
  const HOME_DIR = '/home/user';

  beforeEach(() => {
    vi.spyOn(os, 'homedir').mockReturnValue(HOME_DIR);
    vi.mocked(pathsHomedir).mockReturnValue(HOME_DIR);
  });

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

  describe('getIdeInstaller', () => {
    it.each([
      { ide: IDE_DEFINITIONS.vscode },
      { ide: IDE_DEFINITIONS.firebasestudio },
    ])('returns a VsCodeInstaller for "$ide.name"', ({ ide }) => {
      const installer = getIdeInstaller(ide);

      expect(installer).not.toBeNull();
      expect(installer?.install).toEqual(expect.any(Function));
    });

    it('returns an AntigravityInstaller for "antigravity"', () => {
      const installer = getIdeInstaller(IDE_DEFINITIONS.antigravity);

      expect(installer).not.toBeNull();
      expect(installer?.install).toEqual(expect.any(Function));
    });
  });

  describe('VsCodeInstaller', () => {
    function setup({
      ide = IDE_DEFINITIONS.vscode,
      existsResult = false,
      execSync = () => '',
      platform = 'linux' as NodeJS.Platform,
    }: {
      ide?: IdeInfo;
      existsResult?: boolean;
      execSync?: () => string;
      platform?: NodeJS.Platform;
    } = {}) {
      vi.spyOn(child_process, 'execSync').mockImplementation(execSync);
      vi.spyOn(fs, 'existsSync').mockReturnValue(existsResult);
      const installer = getIdeInstaller(ide, platform)!;

      return { installer };
    }

    describe('install', () => {
      it.each([
        {
          platform: 'win32' as NodeJS.Platform,
          expectedLookupPaths: [
            path.join('C:\\Program Files', 'Microsoft VS Code/bin/code.cmd'),
            path.join(
              HOME_DIR,
              '/AppData/Local/Programs/Microsoft VS Code/bin/code.cmd',
            ),
          ],
        },
        {
          platform: 'darwin' as NodeJS.Platform,
          expectedLookupPaths: [
            '/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
            path.join(HOME_DIR, 'Library/Application Support/Code/bin/code'),
          ],
        },
        {
          platform: 'linux' as NodeJS.Platform,
          expectedLookupPaths: ['/usr/share/code/bin/code'],
        },
      ])(
        'identifies the path to code cli on platform: $platform',
        async ({ platform, expectedLookupPaths }) => {
          const { installer } = setup({
            platform,
            execSync: () => {
              throw new Error('Command not found'); // `code` is not in PATH
            },
          });
          await installer.install();
          for (const [idx, path] of expectedLookupPaths.entries()) {
            expect(fs.existsSync).toHaveBeenNthCalledWith(idx + 1, path);
          }
        },
      );

      it('installs the extension using code cli', async () => {
        const { installer } = setup({
          platform: 'linux',
        });
        await installer.install();
        expect(child_process.spawnSync).toHaveBeenCalledWith(
          'code',
          [
            '--install-extension',
            'google.gemini-cli-vscode-ide-companion',
            '--force',
          ],
          { stdio: 'pipe', shell: false },
        );
      });

      it('installs the extension using code cli on windows', async () => {
        const { installer } = setup({
          platform: 'win32',
          execSync: () => 'C:\\Program Files\\Microsoft VS Code\\bin\\code.cmd',
        });
        await installer.install();
        expect(child_process.spawnSync).toHaveBeenCalledWith(
          'C:\\Program Files\\Microsoft VS Code\\bin\\code.cmd',
          [
            '--install-extension',
            'google.gemini-cli-vscode-ide-companion',
            '--force',
          ],
          { stdio: 'pipe', shell: true },
        );
      });

      it.each([
        {
          ide: IDE_DEFINITIONS.vscode,
          expectedMessage:
            'VS Code companion extension was installed successfully',
        },
        {
          ide: IDE_DEFINITIONS.firebasestudio,
          expectedMessage:
            'Firebase Studio companion extension was installed successfully',
        },
      ])(
        'returns that the cli was installed successfully',
        async ({ ide, expectedMessage }) => {
          const { installer } = setup({ ide });
          const result = await installer.install();
          expect(result.success).toBe(true);
          expect(result.message).toContain(expectedMessage);
        },
      );

      it.each([
        {
          ide: IDE_DEFINITIONS.vscode,
          expectedErr: 'VS Code CLI not found',
        },
        {
          ide: IDE_DEFINITIONS.firebasestudio,
          expectedErr: 'Firebase Studio CLI not found',
        },
      ])(
        'should return a failure message if $ide is not installed',
        async ({ ide, expectedErr }) => {
          const { installer } = setup({
            ide,
            execSync: () => {
              throw new Error('Command not found');
            },
            existsResult: false,
          });
          const result = await installer.install();
          expect(result.success).toBe(false);
          expect(result.message).toContain(expectedErr);
        },
      );
    });
  });

  describe('PositronInstaller', () => {
    function setup({
      execSync = () => '',
      platform = 'linux' as NodeJS.Platform,
      existsResult = false,
    }: {
      execSync?: () => string;
      platform?: NodeJS.Platform;
      existsResult?: boolean;
    } = {}) {
      vi.spyOn(child_process, 'execSync').mockImplementation(execSync);
      vi.spyOn(fs, 'existsSync').mockReturnValue(existsResult);
      const installer = getIdeInstaller(IDE_DEFINITIONS.positron, platform)!;

      return { installer };
    }

    it('installs the extension', async () => {
      vi.stubEnv('POSITRON', '1');
      const { installer } = setup({});
      const result = await installer.install();

      expect(result.success).toBe(true);
      expect(child_process.spawnSync).toHaveBeenCalledWith(
        'positron',
        [
          '--install-extension',
          'google.gemini-cli-vscode-ide-companion',
          '--force',
        ],
        { stdio: 'pipe', shell: false },
      );
    });

    it('returns a failure message if the cli is not found', async () => {
      const { installer } = setup({
        execSync: () => {
          throw new Error('Command not found');
        },
      });
      const result = await installer.install();

      expect(result.success).toBe(false);
      expect(result.message).toContain('Positron CLI not found');
    });
  });
});

describe('AntigravityInstaller', () => {
  function setup({
    execSync = () => '',
    platform = 'linux' as NodeJS.Platform,
  }: {
    execSync?: () => string;
    platform?: NodeJS.Platform;
  } = {}) {
    vi.spyOn(child_process, 'execSync').mockImplementation(execSync);
    const installer = getIdeInstaller(IDE_DEFINITIONS.antigravity, platform)!;

    return { installer };
  }

  it('installs the extension using the alias', async () => {
    vi.stubEnv('ANTIGRAVITY_CLI_ALIAS', 'agy');
    const { installer } = setup({});
    const result = await installer.install();

    expect(result.success).toBe(true);
    expect(child_process.spawnSync).toHaveBeenCalledWith(
      'agy',
      [
        '--install-extension',
        'google.gemini-cli-vscode-ide-companion',
        '--force',
      ],
      { stdio: 'pipe', shell: false },
    );
  });

  it('ignores an unsafe alias and falls back to safe commands', async () => {
    vi.stubEnv('ANTIGRAVITY_CLI_ALIAS', 'agy;malicious_command');
    const { installer } = setup();
    vi.mocked(child_process.execSync).mockImplementationOnce(() => 'agy');

    const result = await installer.install();

    expect(result.success).toBe(true);
    expect(child_process.execSync).toHaveBeenCalledTimes(1);
    expect(child_process.execSync).toHaveBeenCalledWith('command -v agy', {
      stdio: 'ignore',
    });
    expect(child_process.spawnSync).toHaveBeenCalledWith(
      'agy',
      [
        '--install-extension',
        'google.gemini-cli-vscode-ide-companion',
        '--force',
      ],
      { stdio: 'pipe', shell: false },
    );
  });

  it('falls back to antigravity when agy is unavailable on linux', async () => {
    vi.stubEnv('ANTIGRAVITY_CLI_ALIAS', 'agy');
    const { installer } = setup();
    vi.mocked(child_process.execSync)
      .mockImplementationOnce(() => {
        throw new Error('Command not found');
      })
      .mockImplementationOnce(() => 'antigravity');

    const result = await installer.install();

    expect(result.success).toBe(true);
    expect(child_process.execSync).toHaveBeenNthCalledWith(
      1,
      'command -v agy',
      {
        stdio: 'ignore',
      },
    );
    expect(child_process.execSync).toHaveBeenNthCalledWith(
      2,
      'command -v antigravity',
      { stdio: 'ignore' },
    );
    expect(child_process.spawnSync).toHaveBeenCalledWith(
      'antigravity',
      [
        '--install-extension',
        'google.gemini-cli-vscode-ide-companion',
        '--force',
      ],
      { stdio: 'pipe', shell: false },
    );
  });

  it('falls back to antigravity.cmd when agy.cmd is unavailable on windows', async () => {
    vi.stubEnv('ANTIGRAVITY_CLI_ALIAS', 'agy.cmd');
    const { installer } = setup({
      platform: 'win32',
    });
    vi.mocked(child_process.execSync)
      .mockImplementationOnce(() => {
        throw new Error('Command not found');
      })
      .mockImplementationOnce(
        () => 'C:\\Program Files\\Antigravity\\bin\\antigravity.cmd',
      );

    const result = await installer.install();

    expect(result.success).toBe(true);
    expect(child_process.execSync).toHaveBeenNthCalledWith(
      1,
      'where.exe agy.cmd',
    );
    expect(child_process.execSync).toHaveBeenNthCalledWith(
      2,
      'where.exe antigravity.cmd',
    );
    expect(child_process.spawnSync).toHaveBeenCalledWith(
      'C:\\Program Files\\Antigravity\\bin\\antigravity.cmd',
      [
        '--install-extension',
        'google.gemini-cli-vscode-ide-companion',
        '--force',
      ],
      { stdio: 'pipe', shell: true },
    );
  });

  it('falls back to default commands if the alias is not set', async () => {
    vi.stubEnv('ANTIGRAVITY_CLI_ALIAS', '');
    const { installer } = setup({});
    const result = await installer.install();

    expect(result.success).toBe(true);
  });

  it('returns a failure message if the command is not found', async () => {
    vi.stubEnv('ANTIGRAVITY_CLI_ALIAS', 'not-a-command');
    const { installer } = setup({
      execSync: () => {
        throw new Error('Command not found');
      },
    });
    const result = await installer.install();

    expect(result.success).toBe(false);
    expect(result.message).toContain('Antigravity CLI not found');
    expect(result.message).toContain('agy, antigravity');
  });
});
