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

import {
  describe,
  it,
  expect,
  vi,
  afterEach,
  beforeEach,
  type Mock,
} from 'vitest';
import { getIdeProcessInfo } from './process-utils.js';
import os from 'node:os';

const mockedExec = vi.hoisted(() => vi.fn());
vi.mock('node:util', () => ({
  promisify: vi.fn().mockReturnValue(mockedExec),
}));
vi.mock('node:os', () => ({
  default: {
    platform: vi.fn(),
  },
}));

describe('getIdeProcessInfo', () => {
  beforeEach(() => {
    Object.defineProperty(process, 'pid', { value: 1000, configurable: true });
    mockedExec.mockReset();
  });

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

  describe('GEMINI_CLI_IDE_PID override', () => {
    it('should use GEMINI_CLI_IDE_PID and fetch command on Unix', async () => {
      (os.platform as Mock).mockReturnValue('linux');
      vi.stubEnv('GEMINI_CLI_IDE_PID', '12345');
      mockedExec.mockResolvedValueOnce({ stdout: '0 my-ide-command' }); // getProcessInfo result

      const result = await getIdeProcessInfo();

      expect(result).toEqual({ pid: 12345, command: 'my-ide-command' });
      expect(mockedExec).toHaveBeenCalledWith(
        expect.stringContaining('ps -o ppid=,command= -p 12345'),
      );
    });

    it('should use GEMINI_CLI_IDE_PID and fetch command on Windows', async () => {
      (os.platform as Mock).mockReturnValue('win32');
      vi.stubEnv('GEMINI_CLI_IDE_PID', '54321');
      const processes = [
        {
          ProcessId: 54321,
          ParentProcessId: 0,
          Name: 'Code.exe',
          CommandLine: 'C:\\Program Files\\VSCode\\Code.exe',
        },
      ];
      mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(processes) });

      const result = await getIdeProcessInfo();

      expect(result).toEqual({
        pid: 54321,
        command: 'C:\\Program Files\\VSCode\\Code.exe',
      });
      expect(mockedExec).toHaveBeenCalledWith(
        expect.stringContaining(
          'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine',
        ),
        expect.anything(),
      );
    });
  });

  describe('on Unix', () => {
    it('should traverse up to find the shell and return grandparent process info', async () => {
      (os.platform as Mock).mockReturnValue('linux');
      // process (1000) -> shell (800) -> IDE (700)
      mockedExec
        .mockResolvedValueOnce({ stdout: '800 /bin/bash' }) // pid 1000 -> ppid 800 (shell)
        .mockResolvedValueOnce({ stdout: '700 /usr/lib/vscode/code' }) // pid 800 -> ppid 700 (IDE)
        .mockResolvedValueOnce({ stdout: '700 /usr/lib/vscode/code' }); // get command for pid 700

      const result = await getIdeProcessInfo();

      expect(result).toEqual({ pid: 700, command: '/usr/lib/vscode/code' });
    });

    it('should return parent process info if grandparent lookup fails', async () => {
      (os.platform as Mock).mockReturnValue('linux');
      mockedExec
        .mockResolvedValueOnce({ stdout: '800 /bin/bash' }) // pid 1000 -> ppid 800 (shell)
        .mockRejectedValueOnce(new Error('ps failed')) // lookup for ppid of 800 fails
        .mockResolvedValueOnce({ stdout: '800 /bin/bash' }); // get command for pid 800

      const result = await getIdeProcessInfo();
      expect(result).toEqual({ pid: 800, command: '/bin/bash' });
    });
  });

  describe('on Windows', () => {
    it('should traverse up and find the great-grandchild of the root process', async () => {
      (os.platform as Mock).mockReturnValue('win32');
      // process (1000) -> powershell (900) -> code (800) -> wininit (700) -> root (0)
      // Ancestors: [1000, 900, 800, 700]
      // Target (great-grandchild of root): 900
      const processes = [
        {
          ProcessId: 1000,
          ParentProcessId: 900,
          Name: 'node.exe',
          CommandLine: 'node.exe',
        },
        {
          ProcessId: 900,
          ParentProcessId: 800,
          Name: 'powershell.exe',
          CommandLine: 'powershell.exe',
        },
        {
          ProcessId: 800,
          ParentProcessId: 700,
          Name: 'code.exe',
          CommandLine: 'code.exe',
        },
        {
          ProcessId: 700,
          ParentProcessId: 0,
          Name: 'wininit.exe',
          CommandLine: 'wininit.exe',
        },
      ];
      mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(processes) });

      const result = await getIdeProcessInfo();
      expect(result).toEqual({ pid: 900, command: 'powershell.exe' });
      expect(mockedExec).toHaveBeenCalledWith(
        expect.stringContaining('Get-CimInstance Win32_Process'),
        expect.anything(),
      );
    });

    it('should handle short process chains', async () => {
      (os.platform as Mock).mockReturnValue('win32');
      // process (1000) -> root (0)
      const processes = [
        {
          ProcessId: 1000,
          ParentProcessId: 0,
          Name: 'node.exe',
          CommandLine: 'node.exe',
        },
      ];
      mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(processes) });

      const result = await getIdeProcessInfo();
      expect(result).toEqual({ pid: 1000, command: 'node.exe' });
    });

    it('should handle PowerShell failure gracefully', async () => {
      (os.platform as Mock).mockReturnValue('win32');
      mockedExec.mockRejectedValueOnce(new Error('PowerShell failed'));
      // Fallback to getProcessInfo for current PID
      mockedExec.mockResolvedValueOnce({ stdout: '' }); // ps command fails on windows

      const result = await getIdeProcessInfo();
      expect(result).toEqual({ pid: 1000, command: '' });
    });

    it('should handle malformed JSON output gracefully', async () => {
      (os.platform as Mock).mockReturnValue('win32');
      mockedExec.mockResolvedValueOnce({ stdout: '{"invalid":json}' });
      // Fallback to getProcessInfo for current PID
      mockedExec.mockResolvedValueOnce({ stdout: '' });

      const result = await getIdeProcessInfo();
      expect(result).toEqual({ pid: 1000, command: '' });
    });

    it('should handle single process output from ConvertTo-Json', async () => {
      (os.platform as Mock).mockReturnValue('win32');
      const process = {
        ProcessId: 1000,
        ParentProcessId: 0,
        Name: 'node.exe',
        CommandLine: 'node.exe',
      };
      mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(process) });

      const result = await getIdeProcessInfo();
      expect(result).toEqual({ pid: 1000, command: 'node.exe' });
    });

    it('should handle missing process in map during traversal', async () => {
      (os.platform as Mock).mockReturnValue('win32');
      // process (1000) -> parent (900) -> missing (800)
      const processes = [
        {
          ProcessId: 1000,
          ParentProcessId: 900,
          Name: 'node.exe',
          CommandLine: 'node.exe',
        },
        {
          ProcessId: 900,
          ParentProcessId: 800,
          Name: 'parent.exe',
          CommandLine: 'parent.exe',
        },
      ];
      mockedExec.mockResolvedValueOnce({ stdout: JSON.stringify(processes) });

      const result = await getIdeProcessInfo();
      // Ancestors: [1000, 900]. Length < 3, returns last (900)
      expect(result).toEqual({ pid: 900, command: 'parent.exe' });
    });
  });
});
