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

from collections.abc import Callable
import dataclasses
from typing import Any
import unittest
from unittest import mock

from telemetry.internal.platform import gpu_info

from gpu_tests import common_typing as ct
from gpu_tests import gpu_helper


# pylint: disable=too-many-arguments
def CreateGpuDeviceDict(
  vendor_id: int | None = None,
  device_id: int | None = None,
  sub_sys_id: int | None = None,
  revision: int | None = None,
  vendor_string: str | None = None,
  device_string: str | None = None,
  driver_vendor: str | None = None,
  driver_version: str | None = None,
) -> dict[str, str | int]:
  return {
    'vendor_id': vendor_id or 0,
    'device_id': device_id or 0,
    'sub_sys_id': sub_sys_id or 0,
    'revision': revision or 0,
    'vendor_string': 'vendor_string'
    if vendor_string is None
    else vendor_string,
    'device_string': 'device_string'
    if device_string is None
    else device_string,
    'driver_vendor': 'driver_vendor'
    if driver_vendor is None
    else driver_vendor,
    'driver_version': 'driver_version'
    if driver_version is None
    else driver_version,
  }


# pylint: enable=too-many-arguments


@dataclasses.dataclass
class TagHelperTestCase:
  """Struct-like class for defining a tag helper test case."""

  expected_result: Any
  device_dict: dict[str, str | int] = ct.EmptyDict()
  aux_attributes: dict[str, Any] = ct.EmptyDict()
  feature_status: dict[str, str] = ct.EmptyDict()
  extra_browser_args: list[str] = ct.EmptyList()


class TagHelpersUnittest(unittest.TestCase):
  def runTagHelperTestWithIndex(
    self,
    tc: TagHelperTestCase,
    test_method: Callable[[gpu_info.GPUInfo | None, int], Any],
  ) -> None:
    """Helper method for running a single tag helper test case w/ index."""
    info = gpu_info.GPUInfo(
      [CreateGpuDeviceDict(**tc.device_dict)],
      tc.aux_attributes,
      tc.feature_status,
      None,
    )
    self.assertEqual(test_method(info, 0), tc.expected_result)

  def runTagHelperTest(
    self,
    tc: TagHelperTestCase,
    test_method: Callable[[gpu_info.GPUInfo | None], Any],
  ) -> None:
    """Helper method for running a single tag helper test case w/o index."""
    info = gpu_info.GPUInfo(
      [CreateGpuDeviceDict(**tc.device_dict)],
      tc.aux_attributes,
      tc.feature_status,
      None,
    )
    self.assertEqual(test_method(info), tc.expected_result)

  def testGetGpuVendorString(self) -> None:
    """Tests all code paths for the GetGpuVendorString() method."""
    cases = [
      # Explicit ID -> AMD.
      TagHelperTestCase(
        'amd',
        {
          'vendor_id': 0x1002,
          'device_string': 'ANGLE (ANGLE gpu, 1, 2)',
          'vendor_string': 'Vendor_gpu 1 2',
        },
      ),
      # Explicit ID -> Intel.
      TagHelperTestCase(
        'intel',
        {
          'vendor_id': 0x8086,
          'device_string': 'ANGLE (ANGLE gpu, 1, 2)',
          'vendor_string': 'Vendor_gpu 1 2',
        },
      ),
      # Explicit ID -> NVIDIA.
      TagHelperTestCase(
        'nvidia',
        {
          'vendor_id': 0x10DE,
          'device_string': 'ANGLE (ANGLE gpu, 1, 2)',
          'vendor_string': 'Vendor_gpu 1 2',
        },
      ),
      # ANGLE vendor string.
      TagHelperTestCase(
        'angle gpu',
        {
          'device_string': 'ANGLE (ANGLE gpu, 1, 2)',
          'vendor_string': 'Vendor_gpu 1 2',
        },
      ),
      # Vendor string.
      TagHelperTestCase('vendor_gpu', {'vendor_string': 'Vendor_gpu 1 2'}),
      # Defined info but unknown.
      TagHelperTestCase('unknown_gpu', {'vendor_string': ''}),
    ]

    for tc in cases:
      self.runTagHelperTestWithIndex(tc, gpu_helper.GetGpuVendorString)

    # Undefined info.
    self.assertEqual(gpu_helper.GetGpuVendorString(None, 0), 'unknown_gpu')

  def testGetGpuDeviceId(self) -> None:
    """Tests all code paths for the GetGpuDeviceId() method."""
    cases = [
      # Explicit device.
      TagHelperTestCase(
        0xFFFF,
        {
          'device_id': 0xFFFF,
          'device_string': 'ANGLE (Vendor, Device, Driver)',
        },
      ),
      # ANGLE device string.
      TagHelperTestCase(
        'Device', {'device_string': 'ANGLE (Vendor, Device, Driver)'}
      ),
      # Device string.
      TagHelperTestCase('Some device', {'device_string': 'Some device'}),
    ]

    for tc in cases:
      self.runTagHelperTestWithIndex(tc, gpu_helper.GetGpuDeviceId)

    # Undefined info.
    self.assertEqual(gpu_helper.GetGpuDeviceId(None, 0), 0)

  def testIntelMasks(self) -> None:
    """Tests the masking methods for determining Intel generation."""
    # Sample of real IDs taken from
    # https://dgpu-docs.intel.com/devices/hardware-table.html
    # Note that 12th gen is referred to as "Xe" or "XeHPG.
    gen_9_ids = {0x1923, 0x3184, 0x3EA4, 0x591C, 0x5A85, 0x9BC8}
    # 0x4F and 0xA7-prefixed samples missing since none were listed.
    gen_12_ids = {0x4C8A, 0x9A40, 0x4905, 0x4680, 0x5690}

    for pci_id in gen_9_ids:
      self.assertTrue(gpu_helper.IsIntelGen9(pci_id))
      self.assertFalse(gpu_helper.IsIntelGen12(pci_id))

    for pci_id in gen_12_ids:
      self.assertTrue(gpu_helper.IsIntelGen12(pci_id))
      self.assertFalse(gpu_helper.IsIntelGen9(pci_id))

  def testGetGpuDriverVendor(self) -> None:
    """Tests all code paths for the GetGpuDriverVendor() method."""
    # Explicit vendor.
    self.runTagHelperTest(
      TagHelperTestCase('vendor', {'driver_vendor': 'vendor'}),
      gpu_helper.GetGpuDriverVendor,
    )
    # Undefined info.
    self.assertEqual(gpu_helper.GetGpuDriverVendor(None), None)

  def testGetGpuDriverVersion(self) -> None:
    """Tests all code paths for the GetGpuDriverVersion() method."""
    # Explicit version.
    self.runTagHelperTest(
      TagHelperTestCase('Some version', {'driver_version': 'Some version'}),
      gpu_helper.GetGpuDriverVersion,
    )
    # Undefined info.
    self.assertEqual(gpu_helper.GetGpuDriverVersion(None), None)

  def testGetANGLERenderer(self) -> None:
    """Tests all code paths for the GetANGLERenderer() method."""
    cases = [
      # No aux attributes.
      TagHelperTestCase('angle-disabled'),
      # Non-ANGLE renderer.
      TagHelperTestCase(
        'angle-disabled', aux_attributes={'gl_renderer': 'renderer'}
      ),
      # D3D11.
      TagHelperTestCase(
        'angle-d3d11', aux_attributes={'gl_renderer': 'ANGLE Direct3D11'}
      ),
      # D3D9.
      TagHelperTestCase(
        'angle-d3d9', aux_attributes={'gl_renderer': 'ANGLE Direct3D9'}
      ),
      # OpenGL ES.
      TagHelperTestCase(
        'angle-opengles', aux_attributes={'gl_renderer': 'ANGLE OpenGL ES'}
      ),
      # OpenGL.
      TagHelperTestCase(
        'angle-opengl', aux_attributes={'gl_renderer': 'ANGLE OpenGL'}
      ),
      # Metal.
      TagHelperTestCase(
        'angle-metal', aux_attributes={'gl_renderer': 'ANGLE Metal'}
      ),
      # SwiftShader, explicitly test that it's chosen over Vulkan.
      TagHelperTestCase(
        'angle-swiftshader',
        aux_attributes={'gl_renderer': 'ANGLE Vulkan SwiftShader'},
      ),
      # Vulkan.
      TagHelperTestCase(
        'angle-vulkan', aux_attributes={'gl_renderer': 'ANGLE Vulkan'}
      ),
    ]

    for tc in cases:
      self.runTagHelperTest(tc, gpu_helper.GetANGLERenderer)

    # Undefined info.
    self.assertEqual(gpu_helper.GetANGLERenderer(None), 'angle-disabled')

  def testGetCommandDecoder(self) -> None:
    """Tests all code paths for the GetcommandDecoder() method."""
    cases = [
      # No aux attributes.
      TagHelperTestCase('no_passthrough'),
      # Validating.
      TagHelperTestCase(
        'no_passthrough', aux_attributes={'passthrough_cmd_decoder': False}
      ),
      # Passthrough.
      TagHelperTestCase(
        'passthrough', aux_attributes={'passthrough_cmd_decoder': True}
      ),
    ]

    for tc in cases:
      self.runTagHelperTest(tc, gpu_helper.GetCommandDecoder)

    # Undefined info.
    self.assertEqual(gpu_helper.GetCommandDecoder(None), 'no_passthrough')

  def testGetSkiaGraphiteStatus(self) -> None:
    """Tests all the code paths for the GetSkiaGraphiteStatus() method."""
    cases = [
      # No feature status.
      TagHelperTestCase('graphite-disabled'),
      # Feature status off.
      TagHelperTestCase(
        'graphite-disabled', feature_status={'skia_graphite': 'disabled'}
      ),
      # Feature status on.
      TagHelperTestCase(
        'graphite-enabled', feature_status={'skia_graphite': 'enabled_on'}
      ),
    ]

    for tc in cases:
      self.runTagHelperTest(tc, gpu_helper.GetSkiaGraphiteStatus)

    # Undefined info.
    self.assertEqual(
      gpu_helper.GetSkiaGraphiteStatus(None), 'graphite-disabled'
    )

  def testGetSkiaRenderer(self) -> None:
    """Tests all code paths for the GetSkiaRenderer() method."""
    cases = [
      # No feature status.
      TagHelperTestCase('renderer-software'),
      # No GPU Compositing.
      TagHelperTestCase(
        'renderer-software', feature_status={'gpu_compositing': 'disabled'}
      ),
      # No renderer.
      TagHelperTestCase(
        'renderer-software', feature_status={'gpu_compositing': 'enabled'}
      ),
      # Vulkan Skia Renderer.
      TagHelperTestCase(
        'renderer-skia-vulkan',
        feature_status={
          'gpu_compositing': 'enabled',
          'vulkan': 'enabled_on',
          'opengl': 'enabled_on',
        },
      ),
      # GL Skia Renderer.
      TagHelperTestCase(
        'renderer-skia-gl',
        feature_status={
          'gpu_compositing': 'enabled',
          'vulkan': 'enabled_off',
          'opengl': 'enabled_on',
        },
      ),
    ]

    for tc in cases:
      self.runTagHelperTest(tc, gpu_helper.GetSkiaRenderer)

    # Undefined info.
    self.assertEqual(gpu_helper.GetSkiaRenderer(None), 'renderer-software')

  def testGetDisplayServer(self) -> None:
    """Tests all code paths for the GetDisplayServer() method."""
    with mock.patch(
      'gpu_tests.util.host_information.IsLinux', return_value=True
    ):
      # Remote platforms.
      for browser_type in gpu_helper.REMOTE_BROWSER_TYPES:
        self.assertEqual(gpu_helper.GetDisplayServer(browser_type), None)
      # X.
      with mock.patch.dict('os.environ', {}, clear=True):
        self.assertEqual(gpu_helper.GetDisplayServer(''), 'display-server-x')
      # Wayland.
      with mock.patch.dict('os.environ', {'WAYLAND_DISPLAY': '1'}, clear=True):
        self.assertEqual(
          gpu_helper.GetDisplayServer(''), 'display-server-wayland'
        )

    with mock.patch(
      'gpu_tests.util.host_information.IsLinux', return_value=False
    ):
      self.assertEqual(gpu_helper.GetDisplayServer(''), None)

  def testGetAsanStatus(self) -> None:
    """Tests all code paths for the GetAsanStatus() method."""
    cases = [
      # No aux attributes.
      TagHelperTestCase('no-asan'),
      # Built without ASan.
      TagHelperTestCase('no-asan', aux_attributes={'is_asan': False}),
      # Built with ASan.
      TagHelperTestCase('asan', aux_attributes={'is_asan': True}),
    ]

    for tc in cases:
      self.runTagHelperTest(tc, gpu_helper.GetAsanStatus)

    # Undefined info.
    self.assertEqual(gpu_helper.GetAsanStatus(None), 'no-asan')

  def testGetTargetCpuStatus(self) -> None:
    """Tests all code paths for the GetTargetCpuStatus() method."""
    cases = [
      # No aux attributes.
      TagHelperTestCase('target-cpu-unknown'),
      # Target CPU specified.
      TagHelperTestCase(
        'target-cpu-32', aux_attributes={'target_cpu_bits': 32}
      ),
    ]

    for tc in cases:
      self.runTagHelperTest(tc, gpu_helper.GetTargetCpuStatus)

    # Undefined info.
    self.assertEqual(gpu_helper.GetTargetCpuStatus(None), 'target-cpu-unknown')

  def testGetClangCoverage(self) -> None:
    """Tests all code paths for the GetClangCoverage() method."""
    cases = [
      # No aux attributes.
      TagHelperTestCase('no-clang-coverage'),
      # Built without Clang coverage.
      TagHelperTestCase(
        'no-clang-coverage', aux_attributes={'is_clang_coverage': False}
      ),
      # Built with Clang coverage.
      TagHelperTestCase(
        'clang-coverage', aux_attributes={'is_clang_coverage': True}
      ),
    ]

    for tc in cases:
      self.runTagHelperTest(tc, gpu_helper.GetClangCoverage)

    # Undefined info.
    self.assertEqual(gpu_helper.GetClangCoverage(None), 'no-clang-coverage')


class ReplaceTagsUnittest(unittest.TestCase):
  def testSubstringReplacement(self) -> None:
    tags = ['some_tag', 'some-nvidia-corporation', 'another_tag']
    self.assertEqual(
      gpu_helper.ReplaceTags(tags), ['some_tag', 'some-nvidia', 'another_tag']
    )

  def testRegexReplacement(self) -> None:
    tags = [
      'some_tag',
      'google-Vulkan-1.3.0-(SwiftShader-Device-(LLVM-10.0.0)-(0x0000C0DE))',
      'another_tag',
    ]
    self.assertEqual(
      gpu_helper.ReplaceTags(tags), ['some_tag', 'google-vulkan', 'another_tag']
    )


class EvaluateVersionComparisonUnittest(unittest.TestCase):
  def testWindowsIntelUncomparableVersions(self) -> None:
    """Tests Windows Intel comparison when versions cannot be compared."""
    non_ne_operations = ('eq', 'ge', 'gt', 'le', 'lt')
    # Versions should only be comparable with 4 elements.
    self.assertTrue(
      gpu_helper.EvaluateVersionComparison(
        '1.2.3', 'ne', '1.2.3', 'win', 'intel'
      )
    )
    for op in non_ne_operations:
      self.assertFalse(
        gpu_helper.EvaluateVersionComparison(
          '1.2.3', op, '1.2.3', 'win', 'intel'
        )
      )
    self.assertTrue(
      gpu_helper.EvaluateVersionComparison(
        '1.2.3.4.5', 'ne', '1.2.3.4.5', 'win', 'intel'
      )
    )
    for op in non_ne_operations:
      self.assertFalse(
        gpu_helper.EvaluateVersionComparison(
          '1.2.3.4.5', op, '1.2.3.4.5', 'win', 'intel'
        )
      )

  def testWindowsIntelOnlyLastTwoPartsUsed(self) -> None:
    """Tests that only the last two version parts are used on Windows Intel."""
    self.assertTrue(
      gpu_helper.EvaluateVersionComparison(
        '1.2.3.4', 'eq', '2.3.3.4', 'win', 'intel'
      )
    )
    self.assertFalse(
      gpu_helper.EvaluateVersionComparison(
        '1.2.3.4', 'eq', '2.3.5.4', 'win', 'intel'
      )
    )

  def testInvalidOperation(self) -> None:
    """Tests that an error is raised when using an invalid operation."""
    with self.assertRaisesRegex(Exception, 'Invalid operation: foo'):
      gpu_helper.EvaluateVersionComparison('1.2.3.4', 'foo', '2.3.4.5')

  def testEqual(self) -> None:
    """Tests that equality operations work as expected."""
    eq_operations = ('eq', 'ge', 'le')
    for op in eq_operations:
      # Purely numerical.
      self.assertTrue(
        gpu_helper.EvaluateVersionComparison('1.2.3.4', op, '1.2.3.4')
      )

      # Numerical + suffix.
      self.assertTrue(
        gpu_helper.EvaluateVersionComparison('1a.2b.3c.4a', op, '1a.2b.3c.4a')
      )

      # Mismatched length implies 0.
      self.assertTrue(
        gpu_helper.EvaluateVersionComparison('1.2.0.0', op, '1.2')
      )
      self.assertTrue(
        gpu_helper.EvaluateVersionComparison('1.2', op, '1.2.0.0')
      )

      # Failure to parse gets skipped unless in the reference version.
      self.assertTrue(
        gpu_helper.EvaluateVersionComparison('1.2..4', op, '1.2.3.4')
      )
      with self.assertRaises(AssertionError):
        gpu_helper.EvaluateVersionComparison('1.2.3.4', op, '1.2..4')

  def testNotEqual(self) -> None:
    """Tests that the not equal operation works as expected."""
    true_cases = (
      # Purely numerical.
      ('1.2.3.4', '1.2.3'),
      ('1.2.3', '1.2.3.4'),
      # Same suffix, different numerical.
      ('2a.2b.3c.4d', '1a.2b.3c.4d'),
      ('1a.3b.3c.4d', '1a.2b.3c.4d'),
      ('1a.2b.4c.4d', '1a.2b.3c.4d'),
      ('1a.2b.3c.5d', '1a.2b.3c.4d'),
      # Same numerical, different suffix.
      ('1b.2b.3c.4d', '1a.2b.3c.4d'),
      ('1a.2c.3c.4d', '1a.2b.3c.4d'),
      ('1a.2b.3d.4d', '1a.2b.3c.4d'),
      ('1a.2b.3c.4e', '1a.2b.3c.4d'),
    )
    false_cases = (
      # Numerical.
      ('1.2.3.4', '1.2.3.4'),
      # Numerical + suffix.
      ('1a.2b.3c.4d', '1a.2b.3c.4d'),
    )
    for left, right in true_cases:
      self.assertTrue(gpu_helper.EvaluateVersionComparison(left, 'ne', right))
      self.assertTrue(gpu_helper.EvaluateVersionComparison(right, 'ne', left))
    for left, right in false_cases:
      self.assertFalse(gpu_helper.EvaluateVersionComparison(left, 'ne', right))
      self.assertFalse(gpu_helper.EvaluateVersionComparison(right, 'ne', left))

  def testGreater(self) -> None:
    """Tests that greater operations work as expected."""
    gt_operations = ('gt', 'ge')
    for op in gt_operations:
      for left, right in GetGreaterTestCases():
        self.assertTrue(gpu_helper.EvaluateVersionComparison(left, op, right))
        self.assertFalse(gpu_helper.EvaluateVersionComparison(right, op, left))

  def testLess(self) -> None:
    """Tests that less operations work as expected."""
    lt_operations = ('lt', 'le')
    for op in lt_operations:
      # Less than test cases are simply the inverse of greater than test cases,
      # so just reverse the order.
      for right, left in GetGreaterTestCases():
        self.assertTrue(gpu_helper.EvaluateVersionComparison(left, op, right))
        self.assertFalse(gpu_helper.EvaluateVersionComparison(right, op, left))


def GetGreaterTestCases() -> tuple[tuple[str, str], ...]:
  return (
    # Purely numerical.
    ('2.2.3.4', '1.2.3.4'),
    ('1.3.3.4', '1.2.3.4'),
    ('1.2.4.4', '1.2.3.4'),
    ('1.2.3.5', '1.2.3.4'),
    # Same suffix, different numerical.
    ('2a.2b.3c.4d', '1a.2b.3c.4d'),
    ('1a.3b.3c.4d', '1a.2b.3c.4d'),
    ('1a.2b.4c.4d', '1a.2b.3c.4d'),
    ('1a.2b.3c.5d', '1a.2b.3c.4d'),
    # Same numerical, different suffix.
    ('1b.2b.3c.4d', '1a.2b.3c.4d'),
    ('1a.2c.3c.4d', '1a.2b.3c.4d'),
    ('1a.2b.3d.4d', '1a.2b.3c.4d'),
    ('1a.2b.3c.4e', '1a.2b.3c.4d'),
  )


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