#!/usr/bin/env vpython3
# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests for promptfoo_installation."""

import pathlib
import subprocess
import signal
import unittest
from unittest import mock

from pyfakefs import fake_filesystem_unittest

import promptfoo_installation

# pylint: disable=protected-access


class FromCipdPromptfooInstallationUnittest(fake_filesystem_unittest.TestCase):
    """Unit tests for FromCipdPromptfooInstallation."""

    def setUp(self):
        self.setUpPyfakefs()
        run_patcher = mock.patch('promptfoo_installation._run')
        self.mock_run = run_patcher.start()
        self.addCleanup(run_patcher.stop)
        check_call_patcher = mock.patch('subprocess.check_call')
        self.mock_check_call = check_call_patcher.start()
        self.addCleanup(check_call_patcher.stop)

    def test_setup(self):
        """Tests that setup runs the correct cipd commands."""
        promptfoo_installation.FromCipdPromptfooInstallation()

        self.mock_check_call.assert_has_calls(
            [
                mock.call(
                    [
                        'cipd',
                        'init',
                        '-force',
                        str(promptfoo_installation.CIPD_ROOT),
                    ]
                ),
                mock.call(
                    [
                        'cipd',
                        'install',
                        'infra/3pp/tools/nodejs/linux-${arch}',
                        mock.ANY,
                        '-root',
                        promptfoo_installation.CIPD_ROOT,
                        '-log-level',
                        'warning',
                    ],
                    stdout=subprocess.DEVNULL,
                ),
                mock.call(
                    [
                        'cipd',
                        'install',
                        'infra/3pp/npm/promptfoo/linux-${arch}',
                        mock.ANY,
                        '-root',
                        promptfoo_installation.CIPD_ROOT,
                        '-log-level',
                        'warning',
                    ],
                    stdout=subprocess.DEVNULL,
                ),
            ]
        )

    def test_setup_already_installed(self):
        """Tests that setup does not init cipd if already installed."""
        executable_path = (
            promptfoo_installation.CIPD_ROOT
            / 'node_modules'
            / '.bin'
            / 'promptfoo'
        )
        self.fs.create_file(executable_path)
        promptfoo_installation.FromCipdPromptfooInstallation()

        self.mock_check_call.assert_has_calls(
            [
                mock.call(
                    [
                        'cipd',
                        'install',
                        'infra/3pp/tools/nodejs/linux-${arch}',
                        mock.ANY,
                        '-root',
                        promptfoo_installation.CIPD_ROOT,
                        '-log-level',
                        'warning',
                    ],
                    stdout=subprocess.DEVNULL,
                ),
                mock.call(
                    [
                        'cipd',
                        'install',
                        'infra/3pp/npm/promptfoo/linux-${arch}',
                        mock.ANY,
                        '-root',
                        promptfoo_installation.CIPD_ROOT,
                        '-log-level',
                        'warning',
                    ],
                    stdout=subprocess.DEVNULL,
                ),
            ]
        )
        # Check that 'cipd init' was NOT called.
        for call in self.mock_check_call.call_args_list:
            self.assertNotIn('init', call.args[0])

    def test_installed_true(self):
        """Tests that installed is true when the executable exists."""
        executable_path = (
            promptfoo_installation.CIPD_ROOT
            / 'node_modules'
            / '.bin'
            / 'promptfoo'
        )
        self.fs.create_file(executable_path)
        installation = promptfoo_installation.FromCipdPromptfooInstallation()
        self.assertTrue(installation.installed)

    def test_installed_false(self):
        """Tests that installed is false when the executable does not exist."""
        installation = promptfoo_installation.FromCipdPromptfooInstallation()
        self.assertFalse(installation.installed)

    def test_run(self):
        """Tests that run calls the promptfoo executable."""
        executable_path = (
            promptfoo_installation.CIPD_ROOT
            / 'node_modules'
            / '.bin'
            / 'promptfoo'
        )
        self.fs.create_file(executable_path)
        node_path = promptfoo_installation.CIPD_ROOT / 'bin' / 'node'
        self.fs.create_file(node_path)
        installation = promptfoo_installation.FromCipdPromptfooInstallation()
        installation.run(['eval', '-c', 'config.yaml'], cwd='/tmp/test')
        self.mock_run.assert_called_once_with(
            [str(node_path), str(executable_path), 'eval', '-c', 'config.yaml'],
            cwd='/tmp/test',
        )


class PreinstalledPromptfooInstallationUnittest(
    fake_filesystem_unittest.TestCase
):
    """Unit tests for PreinstalledPromptfooInstallation."""

    def setUp(self):
        self.setUpPyfakefs()
        run_patcher = mock.patch('promptfoo_installation._run')
        self.mock_run = run_patcher.start()
        self.addCleanup(run_patcher.stop)

    def test_installed_true(self):
        """Tests that installed is true when the executable exists."""
        self.fs.create_file('/tmp/promptfoo')
        installation = promptfoo_installation.PreinstalledPromptfooInstallation(
            pathlib.Path('/tmp/promptfoo')
        )
        self.assertTrue(installation.installed)

    def test_installed_false(self):
        """Tests that installed is false when the executable does not exist."""
        installation = promptfoo_installation.PreinstalledPromptfooInstallation(
            pathlib.Path('/tmp/promptfoo')
        )
        self.assertFalse(installation.installed)

    def test_run(self):
        """Tests that run calls the promptfoo executable."""
        self.fs.create_file('/tmp/promptfoo')
        installation = promptfoo_installation.PreinstalledPromptfooInstallation(
            pathlib.Path('/tmp/promptfoo')
        )
        installation.run(['eval', '-c', 'config.yaml'], cwd='/tmp/test')
        self.mock_run.assert_called_once_with(
            [str(pathlib.Path('/tmp/promptfoo')), 'eval', '-c', 'config.yaml'],
            cwd='/tmp/test',
        )


class RunUnittest(unittest.TestCase):
    """Unit tests for _run."""

    @mock.patch('sys.platform', 'win32')
    @mock.patch('subprocess.run')
    def test_win32(self, mock_run):
        mock_run.return_value = subprocess.CompletedProcess(
            ['cmd'], 0, 'out', ''
        )
        result = promptfoo_installation._run(['cmd'], cwd='/tmp')
        mock_run.assert_called_once_with(
            ['cmd'],
            cwd='/tmp',
            check=False,
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
        )
        self.assertEqual(result.returncode, 0)
        self.assertEqual(result.stdout, 'out')

    @mock.patch('signal.SIGKILL', 9, create=True)
    @mock.patch('sys.platform', 'linux')
    @mock.patch('os.killpg', create=True)
    @mock.patch('subprocess.Popen')
    def test_unix(self, mock_popen, mock_killpg):
        mock_proc = mock.MagicMock()
        mock_proc.pid = 12345
        mock_proc.args = ['cmd']
        mock_proc.returncode = 0
        mock_proc.communicate.return_value = ('out', '')
        mock_proc.__enter__.return_value = mock_proc
        mock_popen.return_value = mock_proc

        result = promptfoo_installation._run(['cmd'], cwd='/tmp')

        mock_popen.assert_called_once_with(
            ['cmd'],
            cwd='/tmp',
            text=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            start_new_session=True,
        )
        mock_proc.communicate.assert_called_once()
        mock_killpg.assert_called_once_with(12345, signal.SIGKILL)
        self.assertEqual(result.returncode, 0)
        self.assertEqual(result.stdout, 'out')

    @mock.patch('signal.SIGKILL', 9, create=True)
    @mock.patch('sys.platform', 'linux')
    @mock.patch('os.killpg', create=True)
    @mock.patch('subprocess.Popen')
    def test_unix_killpg_oserror(self, mock_popen, mock_killpg):
        mock_proc = mock.MagicMock()
        mock_proc.pid = 12345
        mock_proc.args = ['cmd']
        mock_proc.returncode = 0
        mock_proc.communicate.return_value = ('out', '')
        mock_proc.__enter__.return_value = mock_proc
        mock_popen.return_value = mock_proc
        mock_killpg.side_effect = OSError('Process not found')

        result = promptfoo_installation._run(['cmd'], cwd='/tmp')

        mock_killpg.assert_called_once_with(12345, signal.SIGKILL)
        self.assertEqual(result.returncode, 0)
        self.assertEqual(result.stdout, 'out')


if __name__ == '__main__':
    unittest.main()
