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

import contextlib
import io
import pathlib
import unittest
from unittest.mock import patch

from pyfakefs import fake_filesystem_unittest

import code_coverage_installer


class CodeCoverageInstallerTest(fake_filesystem_unittest.TestCase):
  """Tests individual filesystem verification and setup helper functions."""

  def setUp(self) -> None:
    """Sets up virtual filesystem, stream redirection, and command mock."""
    self.setUpPyfakefs()
    self.src_root = pathlib.Path('/src')
    self.fs.create_dir('/src')

    self.enterContext(contextlib.redirect_stdout(io.StringIO()))
    self.enterContext(contextlib.redirect_stderr(io.StringIO()))

    self.run_command_patcher = patch('code_coverage_installer.run_command')
    self.mock_run_command = self.run_command_patcher.start()
    self.addCleanup(self.run_command_patcher.stop)

  def test_verify_chromium_checkout_success(self) -> None:
    """Verifies checkout succeeds when DEPS file exists."""
    self.fs.create_file('/src/DEPS')
    result = code_coverage_installer.verify_chromium_checkout(self.src_root)
    self.assertTrue(result)

  def test_verify_chromium_checkout_missing_deps(self) -> None:
    """Verifies checkout fails when DEPS file is missing."""
    result = code_coverage_installer.verify_chromium_checkout(self.src_root)
    self.assertFalse(result)

  def test_verify_llvm_tools_success(self) -> None:
    """Verifies LLVM tools check succeeds when both binaries exist."""
    self.fs.create_file(
      '/src/third_party/llvm-build/Release+Asserts/bin/llvm-cov'
    )
    self.fs.create_file(
      '/src/third_party/llvm-build/Release+Asserts/bin/llvm-profdata'
    )
    result = code_coverage_installer.verify_llvm_tools(self.src_root)
    self.assertTrue(result)

  def test_verify_llvm_tools_missing(self) -> None:
    """Verifies LLVM tools check fails when binaries are missing."""
    result = code_coverage_installer.verify_llvm_tools(self.src_root)
    self.assertFalse(result)

  def test_verify_recipes_success(self) -> None:
    """Verifies recipes check succeeds when recipe module folder exists."""
    self.fs.create_dir('/infra_dir/build/recipes/recipe_modules/code_coverage')
    infra_dir = pathlib.Path('/infra_dir')
    result = code_coverage_installer.verify_recipes(infra_dir)
    self.assertTrue(result)

  def test_verify_recipes_missing(self) -> None:
    """Verifies recipes check fails when recipe module folder is missing."""
    infra_dir = pathlib.Path('/infra_dir')
    result = code_coverage_installer.verify_recipes(infra_dir)
    self.assertFalse(result)

  def test_verify_service_success(self) -> None:
    """Verifies FindIt service check succeeds when service directory exists."""
    self.fs.create_dir('/infra_dir/infra/appengine/findit')
    infra_dir = pathlib.Path('/infra_dir')
    result = code_coverage_installer.verify_service(infra_dir)
    self.assertTrue(result)

  def test_verify_service_missing(self) -> None:
    """Verifies FindIt service check fails when service directory is missing."""
    infra_dir = pathlib.Path('/infra_dir')
    result = code_coverage_installer.verify_service(infra_dir)
    self.assertFalse(result)

  def test_setup_infra_gclient_exists_sync_success(self) -> None:
    """Verifies running gclient sync when .gclient already exists."""
    infra_dir = pathlib.Path('/infra_dir')
    self.fs.create_file('/infra_dir/.gclient')
    self.mock_run_command.return_value = 0
    result = code_coverage_installer.setup_infra(infra_dir)
    self.assertTrue(result)
    self.mock_run_command.assert_called_once_with(
      ['gclient', 'sync'], cwd=infra_dir
    )

  def test_setup_infra_gclient_exists_sync_failure(self) -> None:
    """Verifies setup fails when gclient sync returns non-zero code."""
    infra_dir = pathlib.Path('/infra_dir')
    self.fs.create_file('/infra_dir/.gclient')
    self.mock_run_command.return_value = 1
    result = code_coverage_installer.setup_infra(infra_dir)
    self.assertFalse(result)

  def test_setup_infra_no_gclient_fetch_success(self) -> None:
    """Verifies fetch infra_superproject runs when .gclient is missing."""
    infra_dir = pathlib.Path('/infra_dir')
    self.mock_run_command.return_value = 0
    result = code_coverage_installer.setup_infra(infra_dir)
    self.assertTrue(result)
    self.mock_run_command.assert_called_once_with(
      ['fetch', 'infra_superproject'], cwd=infra_dir
    )

  def test_setup_infra_no_gclient_fetch_failure(self) -> None:
    """Verifies setup fails when fetch infra_superproject returns non-zero."""
    infra_dir = pathlib.Path('/infra_dir')
    self.mock_run_command.return_value = 1
    result = code_coverage_installer.setup_infra(infra_dir)
    self.assertFalse(result)

  def test_fs_permission_error_handled(self) -> None:
    """Verifies filesystem permission errors are caught gracefully."""
    with patch(
      'pathlib.Path.exists', side_effect=PermissionError('Access Denied')
    ):
      result = code_coverage_installer.verify_chromium_checkout(self.src_root)
    self.assertFalse(result)

  def test_verify_gemini_md_creates_new(self) -> None:
    """Verifies creating GEMINI.md with template import when file missing."""
    result = code_coverage_installer.verify_gemini_md(self.src_root)
    self.assertTrue(result)
    content = (self.src_root / 'GEMINI.md').read_text(encoding='utf-8')
    self.assertIn('@agents/prompts/templates/code_coverage.md', content)

  def test_verify_gemini_md_appends_existing(self) -> None:
    """Verifies appending template import when GEMINI.md already exists."""
    gemini_path = self.src_root / 'GEMINI.md'
    gemini_path.write_text('# Existing prompt\n', encoding='utf-8')
    result = code_coverage_installer.verify_gemini_md(self.src_root)
    self.assertTrue(result)
    content = gemini_path.read_text(encoding='utf-8')
    self.assertIn('# Existing prompt', content)
    self.assertIn('@agents/prompts/templates/code_coverage.md', content)

  def test_verify_gemini_md_already_present(self) -> None:
    """Verifies no duplicate entry added when import is already present."""
    gemini_path = self.src_root / 'GEMINI.md'
    gemini_path.write_text(
      '@agents/prompts/templates/code_coverage.md\n', encoding='utf-8'
    )
    result = code_coverage_installer.verify_gemini_md(self.src_root)
    self.assertTrue(result)


class CodeCoverageInstallerMainTest(unittest.TestCase):
  """Tests full CLI main() workflow with mocked environment verification."""

  def setUp(self) -> None:
    """Initializes all verification patchers to isolate main() CLI execution."""
    self.enterContext(contextlib.redirect_stdout(io.StringIO()))
    self.enterContext(contextlib.redirect_stderr(io.StringIO()))

    self.patcher_argv = patch(
      'sys.argv', ['code_coverage_installer.py', '--infra-dir', '/infra_dir']
    )
    self.patcher_exit = patch('sys.exit', side_effect=SystemExit)
    self.patcher_gemini = patch(
      'code_coverage_installer.verify_gemini_md', return_value=True
    )
    self.patcher_checkout = patch(
      'code_coverage_installer.verify_chromium_checkout'
    )
    self.patcher_llvm = patch('code_coverage_installer.verify_llvm_tools')
    self.patcher_recipes = patch('code_coverage_installer.verify_recipes')
    self.patcher_service = patch('code_coverage_installer.verify_service')
    self.patcher_setup = patch('code_coverage_installer.setup_infra')

    self.mock_checkout = self.patcher_checkout.start()
    self.mock_llvm = self.patcher_llvm.start()
    self.mock_recipes = self.patcher_recipes.start()
    self.mock_service = self.patcher_service.start()
    self.mock_gemini = self.patcher_gemini.start()
    self.mock_setup = self.patcher_setup.start()
    self.mock_exit = self.patcher_exit.start()
    self.patcher_argv.start()

    self.addCleanup(patch.stopall)

  def test_main_checkout_failed(self) -> None:
    """Verifies main() exits with status 1 if Chromium checkout check fails."""
    self.mock_checkout.return_value = False
    with self.assertRaises(SystemExit):
      code_coverage_installer.main()
    self.mock_exit.assert_called_once_with(1)

  def test_main_llvm_failed(self) -> None:
    """Verifies main() exits with status 1 if LLVM tools check fails."""
    self.mock_checkout.return_value = True
    self.mock_llvm.return_value = False
    with self.assertRaises(SystemExit):
      code_coverage_installer.main()
    self.mock_exit.assert_called_once_with(1)

  def test_main_already_fully_set_up(self) -> None:
    """Verifies main() succeeds immediately without setup when checks pass."""
    self.mock_checkout.return_value = True
    self.mock_llvm.return_value = True
    self.mock_recipes.return_value = True
    self.mock_service.return_value = True
    code_coverage_installer.main()
    self.assertEqual(self.mock_exit.call_count, 0)

  def test_main_setup_needed_and_succeeds(self) -> None:
    """Verifies main() runs setup_infra when recipes/service check fails."""
    self.mock_checkout.return_value = True
    self.mock_llvm.return_value = True
    self.mock_recipes.side_effect = [False, True]
    self.mock_service.side_effect = [False, True]
    self.mock_setup.return_value = True
    code_coverage_installer.main()
    self.mock_setup.assert_called_once_with(pathlib.Path('/infra_dir'))
    self.assertEqual(self.mock_exit.call_count, 0)

  def test_main_setup_needed_but_setup_fails(self) -> None:
    """Verifies main() exits with status 1 when setup_infra fails."""
    self.mock_checkout.return_value = True
    self.mock_llvm.return_value = True
    self.mock_recipes.return_value = False
    self.mock_service.return_value = False
    self.mock_setup.return_value = False
    with self.assertRaises(SystemExit):
      code_coverage_installer.main()
    self.mock_setup.assert_called_once_with(pathlib.Path('/infra_dir'))
    self.mock_exit.assert_called_once_with(1)

  def test_main_setup_succeeds_but_re_verification_fails(self) -> None:
    """Verifies main() exits with status 1 if re-verification fails."""
    self.mock_checkout.return_value = True
    self.mock_llvm.return_value = True
    self.mock_recipes.side_effect = [False, False]
    self.mock_service.side_effect = [False, True]
    self.mock_setup.return_value = True
    with self.assertRaises(SystemExit):
      code_coverage_installer.main()
    self.mock_setup.assert_called_once_with(pathlib.Path('/infra_dir'))
    self.mock_exit.assert_called_once_with(1)


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