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

import unittest

import apkanalyzer


class ApkAnalyzerTest(unittest.TestCase):
  def assertEqualLists(self, list1, list2):
    self.assertEqual(set(list1), set(list2))

  def testUndoHierarchicalSizing_Empty(self):
    data = []
    nodes = apkanalyzer.UndoHierarchicalSizing(data)
    self.assertEqual(0, len(nodes))

  def testUndoHierarchicalSizing_TotalSingleRootNode(self):
    data = [
      ('P', '<TOTAL>', 5),
    ]
    nodes = apkanalyzer.UndoHierarchicalSizing(data)
    # No changes expected since there are no child nodes.
    self.assertEqualLists(data, nodes)

  def testUndoHierarchicalSizing_TotalSizeMinusChildNode(self):
    data = [
      ('P', '<TOTAL>', 10),
      ('C', 'child1', 7),
    ]
    nodes = apkanalyzer.UndoHierarchicalSizing(data)
    self.assertEqualLists([
      ('P', '<TOTAL>', 3),
      ('C', 'child1', 7),
    ], nodes)

  def testUndoHierarchicalSizing_SiblingAnonymousClass(self):
    data = [
      ('C', 'class1', 10),
      ('C', 'class1$inner', 8),
    ]
    nodes = apkanalyzer.UndoHierarchicalSizing(data)
    # No change in size expected since these should be siblings.
    self.assertEqualLists(data, nodes)

  def testUndoHierarchicalSizing_MethodsShouldBeChildNodes(self):
    data = [
      ('C', 'class1', 10),
      ('M', 'class1 method', 8),
    ]
    nodes = apkanalyzer.UndoHierarchicalSizing(data)
    self.assertEqualLists([
      ('C', 'class1', 2),
      ('M', 'class1 method', 8),
    ], nodes)

  def testUndoHierarchicalSizing_ClassIsChildNodeOfPackage(self):
    data = [
      ('P', 'package1', 10),
      ('C', 'package1.class1', 10),
    ]
    nodes = apkanalyzer.UndoHierarchicalSizing(data)
    self.assertEqualLists([
      ('C', 'package1.class1', 10),
    ], nodes)

  def testUndoHierarchicalSizing_TotalIncludesAllPackages(self):
    data = [
      ('P', '<TOTAL>', 10),
      ('C', 'class1', 3),
      ('C', 'class2', 4),
      ('C', 'class3', 2),
    ]
    nodes = apkanalyzer.UndoHierarchicalSizing(data)
    self.assertEqualLists([
      ('P', '<TOTAL>', 1),
      ('C', 'class1', 3),
      ('C', 'class2', 4),
      ('C', 'class3', 2),
    ], nodes)

  def testUndoHierarchicalSizing_PackageAndClassSameName(self):
    data = [
      ('P', 'name', 4),
      ('C', 'name.Class', 4),
      ('C', 'name', 2),
    ]
    nodes = apkanalyzer.UndoHierarchicalSizing(data)
    self.assertEqualLists([
      ('C', 'name.Class', 4),
      ('C', 'name', 2),
    ], nodes)

  def assertNormalizedTo(self, class_path, expected_outer_class, expected_name):
    lambda_normalizer = apkanalyzer.LambdaNormalizer()
    actual_outer_class, actual_name = lambda_normalizer.Normalize(
        class_path, class_path)
    self.assertEqual(expected_outer_class, actual_outer_class)
    self.assertEqual(expected_name, actual_name)

  def testLambdaNormalizer_normalLambda(self):
    self.assertNormalizedTo(
        class_path='package.FirebaseInstallationsRegistrar$$Lambda$1',
        expected_outer_class='package.FirebaseInstallationsRegistrar',
        # Always re-number lambdas.
        expected_name='package.FirebaseInstallationsRegistrar$$Lambda$0',
    )

  def testLambdaNormalizer_externalSyntheticLambda(self):
    self.assertNormalizedTo(
        class_path='AnimatedProgressBar$$ExternalSyntheticLambda0',
        expected_outer_class='AnimatedProgressBar',
        expected_name=('$$Outlined$AnimatedProgressBar$$Lambda$0'),
    )

  def testLambdaNormalizer_externalSyntheticOutlineLambda(self):
    self.assertNormalizedTo(
        class_path='AutofillAssistant$$Lambda$2$$ExternalSyntheticOutline0',
        expected_outer_class='AutofillAssistant',
        expected_name=('$$Outlined$AutofillAssistant$$Lambda$0'),
    )

  def testLambdaNormalizer_externalSyntheticLambdaWithExtraText(self):
    self.assertNormalizedTo(
        # pylint: disable=line-too-long
        class_path='ContextMenuCoordinator$$Lambda$1$$ExternalSyntheticThrowCCEIfNotNull0',
        expected_outer_class='ContextMenuCoordinator',
        expected_name=('$$Outlined$ContextMenuCoordinator$$Lambda$0'),
    )

  def testLambdaNormalizer_internalSyntheticLambda(self):
    self.assertNormalizedTo(
        class_path=
        # pylint: disable=line-too-long
        'package.AnimatedProgressBar$$InternalSyntheticLambda$3$81073ff626580374a45b03cb5c962b81fe2a950064649dc98bf006e85619e92f$0',
        expected_outer_class='package.AnimatedProgressBar',
        expected_name='package.AnimatedProgressBar$$Lambda$0',
    )

  def testLambdaNormalizer_internalSyntheticOutlineLambda(self):
    self.assertNormalizedTo(
        class_path=
        # pylint: disable=line-too-long
        'package.ChromeActivity$$Lambda$28$$InternalSyntheticOutline$807$cbe941dd7bdbd82d4d93f21de2ea4e38c4468513f97dc90dcb54501b2fc335a6$0',
        expected_outer_class='package.ChromeActivity',
        expected_name='package.ChromeActivity$$Lambda$0',
    )

  def testLambdaNormalizer_oldLambdaFormat(self):
    self.assertNormalizedTo(
        class_path=
        'org.-$$Lambda$StackAnimation$Nested1$kjevdDQ8V2zqCrdieLqWLHzk',
        expected_outer_class='org.StackAnimation',
        expected_name='org.StackAnimation$Nested1$$Lambda$0',
    )

  def testLambdaNormalizer_oldLambdaFormatWithDexSuffix(self):
    self.assertNormalizedTo(
        class_path=
        'org.-$$Lambda$StackAnimation$Nested1$kjevdDQ8V2zqCrdieLqWLHzk.dex',
        expected_outer_class='org.StackAnimation',
        expected_name='org.StackAnimation$Nested1$$Lambda$0',
    )

  def testLambdaNormalizer_classNameIncludesLambda(self):
    self.assertNormalizedTo(
        class_path='package.DisposableLambdaObserver',
        expected_outer_class='package.DisposableLambdaObserver',
        expected_name='package.DisposableLambdaObserver',
    )

  def testLambdaNormalizer_prefix(self):
    lambda_normalizer = apkanalyzer.LambdaNormalizer()
    name = 'org.-$$Lambda$StackAnimation$Nested1$kjevdeLqWLHzk foo bar'
    package = name.split(' ')[0]
    expected_outer_class = 'org.StackAnimation'
    expected_name = 'org.StackAnimation$Nested1$$Lambda$0 foo bar'
    self.assertEqual((expected_outer_class, expected_name),
                     lambda_normalizer.Normalize(package, name))

  def testLambdaNormalizer_lambdaCounting(self):
    lambda_normalizer = apkanalyzer.LambdaNormalizer()
    name = 'org.-$$Lambda$StackAnimation$Nested1$kjevdDQ8V2zqCrdieLqWLHzk'
    expected_outer_class = 'org.StackAnimation'
    expected_name = 'org.StackAnimation$Nested1$$Lambda$0'
    # Ensure multiple calls to the same class maps to same number.
    self.assertEqual((expected_outer_class, expected_name),
                     lambda_normalizer.Normalize(name, name))
    self.assertEqual((expected_outer_class, expected_name),
                     lambda_normalizer.Normalize(name, name))
    name = 'org.-$$Lambda$StackAnimation$Nested1$kjevdDQ8V2zqCrdieLqWLHzk2'
    expected_name = 'org.StackAnimation$Nested1$$Lambda$1'
    self.assertEqual((expected_outer_class, expected_name),
                     lambda_normalizer.Normalize(name, name))

  def testLambdaNormalizer_multiSameLine(self):
    lambda_normalizer = apkanalyzer.LambdaNormalizer()
    name = ('org.-$$Lambda$StackAnimation$Nested1$kevdDQ8V2zqCrdieLqWLHzk '
            'org.-$$Lambda$Other$kjevdDQ8V2zqCrdieLqWLHzk bar')
    package = name.split(' ')[0]
    expected_outer_class = 'org.StackAnimation'
    expected_name = ('org.StackAnimation$Nested1$$Lambda$0 '
                     'org.-$$Lambda$Other$kjevdDQ8V2zqCrdieLqWLHzk bar')
    self.assertEqual((expected_outer_class, expected_name),
                     lambda_normalizer.Normalize(package, name))
    name = expected_name
    package = name.split(' ')[1]
    expected_outer_class = 'org.Other'
    expected_name = ('org.StackAnimation$Nested1$$Lambda$0 '
                     'org.Other$$Lambda$0 bar')
    self.assertEqual((expected_outer_class, expected_name),
                     lambda_normalizer.Normalize(package, name))

  def testCreateDexSymbol_normal(self):
    name = ('org.StackAnimation org.ChromeAnimation '
            'createReachTopAnimatorSet(org.StackTab[],float)')
    size = 1
    source_map = {}
    lambda_normalizer = apkanalyzer.LambdaNormalizer()
    symbol = apkanalyzer.CreateDexSymbol(name, size, source_map,
                                         lambda_normalizer)
    self.assertEqual('$APK/org/StackAnimation', symbol.object_path)

  def testCreateDexSymbol_classMerged_noSource(self):
    name = ('org.NewClass org.ChromeAnimation '
            'org.OldClass.createReachTopAnimatorSet(org.StackTab[],float)')
    size = 1
    source_map = {}
    lambda_normalizer = apkanalyzer.LambdaNormalizer()
    symbol = apkanalyzer.CreateDexSymbol(name, size, source_map,
                                         lambda_normalizer)
    self.assertEqual('$APK/org/OldClass', symbol.object_path)

  def testCreateDexSymbol_classMerged_withSource(self):
    name = ('org.NewClass org.ChromeAnimation '
            'org.OldClass.createReachTopAnimatorSet(org.StackTab[],float)')
    size = 1
    source_map = {'org.OldClass': 'old_path.java'}
    lambda_normalizer = apkanalyzer.LambdaNormalizer()
    symbol = apkanalyzer.CreateDexSymbol(name, size, source_map,
                                         lambda_normalizer)
    self.assertEqual('$APK/org/OldClass', symbol.object_path)
    self.assertEqual('old_path.java', symbol.source_path)

  def testCreateDexSymbol_classMerged_field(self):
    name = 'org.NewClass int org.OldClass.createReachTopAnimatorSet'
    size = 1
    source_map = {}
    lambda_normalizer = apkanalyzer.LambdaNormalizer()
    symbol = apkanalyzer.CreateDexSymbol(name, size, source_map,
                                         lambda_normalizer)
    self.assertEqual('$APK/org/OldClass', symbol.object_path)

  def testCreateDexSymbol_total(self):
    name = '<TOTAL>'
    size = 1
    source_map = {}
    lambda_normalizer = apkanalyzer.LambdaNormalizer()
    symbol = apkanalyzer.CreateDexSymbol(name, size, source_map,
                                         lambda_normalizer)
    self.assertIsNone(symbol)


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