#!/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 get_task_digest.py."""

import contextlib
import io
import json
import sys
import unittest
from unittest import mock
import get_task_digest as gtd


class GetTaskDigestTest(unittest.TestCase):
  """Unit tests for get_task_digest.py."""

  def setUp(self) -> None:
    """Sets up subprocess run mocks."""
    self.run_patcher = mock.patch('subprocess.run')
    self.mock_run = self.run_patcher.start()
    self.addCleanup(self.run_patcher.stop)

  def test_input_properties_success(self) -> None:
    """Verifies hash extraction from input swarm_hashes."""
    data = {
      'input': {'properties': {'swarm_hashes': {'net_unittests': 'abc123/810'}}}
    }
    mock_proc = mock.MagicMock(returncode=0, stdout=json.dumps(data))
    self.mock_run.return_value = mock_proc
    res = gtd.get_digest_from_properties('123', 'net_unittests')
    self.assertEqual(res, 'abc123')

  def test_output_properties_success(self) -> None:
    """Verifies fallback to output properties when input misses."""
    data_in = {'input': {'properties': {'swarm_hashes': {}}}}
    data_out = {'output': {'properties': {'net_unittests': 'def456/999'}}}
    proc_in = mock.MagicMock(returncode=0, stdout=json.dumps(data_in))
    proc_out = mock.MagicMock(returncode=0, stdout=json.dumps(data_out))
    self.mock_run.side_effect = [proc_in, proc_out]
    res = gtd.get_digest_from_properties('123', 'net_unittests')
    self.assertEqual(res, 'def456')

  def test_output_swarm_hashes(self) -> None:
    """Verifies extraction from output swarm_hashes."""
    data_in = {'input': {'properties': {}}}
    data_out = {
      'output': {
        'properties': {'swarm_hashes': {'net_unittests': 'hash111/50'}}
      }
    }
    proc_in = mock.MagicMock(returncode=0, stdout=json.dumps(data_in))
    proc_out = mock.MagicMock(returncode=0, stdout=json.dumps(data_out))
    self.mock_run.side_effect = [proc_in, proc_out]
    res = gtd.get_digest_from_properties('123', 'net_unittests')
    self.assertEqual(res, 'hash111')

  def test_output_swarming_trigger_properties(self) -> None:
    """Verifies extraction from swarming_trigger_properties."""
    data_in = {'input': {'properties': {}}}
    data_out = {
      'output': {
        'properties': {
          'swarming_trigger_properties': {
            'swarm_hashes': {'net_unittests': 'hash789/100'}
          }
        }
      }
    }
    proc_in = mock.MagicMock(returncode=0, stdout=json.dumps(data_in))
    proc_out = mock.MagicMock(returncode=0, stdout=json.dumps(data_out))
    self.mock_run.side_effect = [proc_in, proc_out]
    res = gtd.get_digest_from_properties('123', 'net_unittests')
    self.assertEqual(res, 'hash789')

  def test_orchestrator_child_build(self) -> None:
    """Verifies fallback to steps scan for child build."""
    data_in1 = {'input': {'properties': {}}}
    data_out1 = {'output': {'properties': {}}}
    data_steps = {
      'steps': [{'summaryMarkdown': '* [8000000000000000002](url)'}]
    }
    data_in2 = {'input': {'properties': {}}}
    data_out2 = {'output': {'properties': {'net_unittests': 'childhash/999'}}}
    proc_in1 = mock.MagicMock(returncode=0, stdout=json.dumps(data_in1))
    proc_out1 = mock.MagicMock(returncode=0, stdout=json.dumps(data_out1))
    proc_steps = mock.MagicMock(returncode=0, stdout=json.dumps(data_steps))
    proc_in2 = mock.MagicMock(returncode=0, stdout=json.dumps(data_in2))
    proc_out2 = mock.MagicMock(returncode=0, stdout=json.dumps(data_out2))
    self.mock_run.side_effect = [
      proc_in1,
      proc_out1,
      proc_steps,
      proc_in2,
      proc_out2,
    ]
    res = gtd.get_digest_from_properties('orch', 'net_unittests')
    self.assertEqual(res, 'childhash')

  def test_bad_json_handling(self) -> None:
    """Verifies JSONDecodeError handling across bb calls."""
    proc_bad = mock.MagicMock(returncode=0, stdout='invalid json')
    self.mock_run.side_effect = [proc_bad, proc_bad, proc_bad]
    res = gtd.get_digest_from_properties('123', 'net_unittests')
    self.assertIsNone(res)

  def test_bb_cli_failure(self) -> None:
    """Verifies handling when bb CLI returns non-zero code."""
    proc_fail = mock.MagicMock(returncode=1, stdout='')
    self.mock_run.side_effect = [proc_fail, proc_fail, proc_fail]
    res = gtd.get_digest_from_properties('123', 'net_unittests')
    self.assertIsNone(res)

  @mock.patch('get_task_digest.get_digest_from_properties', return_value=None)
  def test_main_not_found(self, _) -> None:
    """Verifies sys.exit(1) when digest is missing."""
    err_buf = io.StringIO()
    with mock.patch.object(
      sys, 'argv', ['gtd.py', '--build', '1', '--step', 's']
    ):
      with contextlib.redirect_stderr(err_buf):
        with self.assertRaises(SystemExit) as cm:
          gtd.main()
    self.assertEqual(cm.exception.code, 1)
    self.assertIn('not found', err_buf.getvalue())

  @mock.patch(
    'get_task_digest.get_digest_from_properties', return_value='hash789'
  )
  def test_main_success(self, _) -> None:
    """Verifies digest is printed to stdout."""
    out_buf = io.StringIO()
    with mock.patch.object(
      sys, 'argv', ['gtd.py', '--build', '1', '--step', 's']
    ):
      with contextlib.redirect_stdout(out_buf):
        gtd.main()
    self.assertIn('hash789', out_buf.getvalue())


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