# 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.
"""Tests for memory_usage_view.py"""

import json
import os
import pandas as pd
import pathlib
import sys
import unittest

_SRC_PATH = pathlib.Path(__file__).resolve().parents[4]
sys.path.append(str(_SRC_PATH / 'tools/android'))
from colabutils.memory_usage.memory_usage_view import (
    MemoryUsageView,
    TreeNode,
    _aggregate_nodes,
    _zip_by_name,
    _compare_node_lists,
    _prettify_size,
)

_REALISTIC_JSON = """
[
  {
    "name": "main",
    "value": 0,
    "children": [
      {
        "name": "foo",
        "value": 0,
        "children": [
          {
            "name": "malloc",
            "value": 8
          }
        ]
      },
      {
        "name": "malloc",
        "value": 16
      },
      {
        "name": "bar",
        "value": 0,
        "children": [
          {
            "name": "malloc",
            "value": 48
          }
        ]
      }
    ]
  }
]
"""

_SIMPLE_JSON_WITH_DELTAS = """
[
  {
    "name": "root1",
    "value": 100,
    "delta": 10,
    "children": [
      {
        "name": "child1_1",
        "value": 50,
        "delta": 5
      },
      {
        "name": "child1_2",
        "value": 30,
        "delta": 3
      }
    ]
  },
  {
    "name": "root2",
    "value": 200,
    "delta": 20
  }
]
"""


class MemoryUsageViewTest(unittest.TestCase):
    def test_json_preserved(self):
        view = MemoryUsageView.from_json(_SIMPLE_JSON_WITH_DELTAS)
        dumped_json = view.to_json()
        self.assertEqual(
            json.loads(_SIMPLE_JSON_WITH_DELTAS), json.loads(dumped_json)
        )

    def test_from_json_defaults(self):
        json_data = '''[{"name": "root"}]'''
        view = MemoryUsageView.from_json(json_data)
        self.assertEqual(len(view.roots), 1)
        self.assertEqual(view.roots[0].name, 'root')
        self.assertEqual(view.roots[0].value, 0)
        self.assertEqual(view.roots[0].delta, 0)
        self.assertEqual(view.roots[0].children, [])

    def test_from_json_empty_name_stays_empty(self):
        json_data = '''[{"name": ""}]'''
        view = MemoryUsageView.from_json(json_data)
        self.assertEqual(len(view.roots), 1)
        self.assertEqual(view.roots[0].name, '')

    def test_to_json_no_children(self):
        node = TreeNode(name='test_node')
        view = MemoryUsageView(roots=[node])
        dumped_json = json.loads(view.to_json())
        expected_json = {'name': 'test_node', 'value': 0, 'delta': 0}
        self.assertEqual(dumped_json, [expected_json])

    def test_from_df(self):
        df_data = {
            'callsite_id': [1, 2, 3],
            'depth': [0, 0, 1],
            'frame_name': ['root_a', 'root_b', 'child_a_1'],
            'parent_callsite_id': [None, None, 1],
            'total_size_bytes': [100, 200, 50],
        }
        df = pd.DataFrame(df_data)
        view = MemoryUsageView.from_df(df, demangle=False, aggregate=False)
        dumped_json = view.to_json()
        expected_json = """
[
  {
    "name": "root_a",
    "value": 100,
    "delta": 0,
    "children": [
      {
        "name": "child_a_1",
        "value": 50,
        "delta": 0
      }
    ]
  },
  {
    "name": "root_b",
    "value": 200,
    "delta": 0
  }
]
"""
        self.assertEqual(json.loads(expected_json), json.loads(dumped_json))
        self.assertEqual(['root_a', 'root_b'], sorted(view.toplevel_names()))

    def test_from_df_empty_name_converts_to_unknown(self):
        df_data = {
            'callsite_id': [1, 2, 3],
            'depth': [0, 0, 1],
            'frame_name': ['root_a', 'root_b', ''],
            'parent_callsite_id': [None, None, 1],
            'total_size_bytes': [100, 200, 50],
        }
        df = pd.DataFrame(df_data)
        view = MemoryUsageView.from_df(df, demangle=False, aggregate=True)
        self.assertEqual(2, len(view.roots))
        root_b_node, root_a_node = view.roots
        self.assertEqual('root_a', root_a_node.name)
        self.assertEqual(100 + 50, root_a_node.value)
        self.assertEqual(1, len(root_a_node.children))
        empty_name_node = root_a_node.children[0]
        self.assertEqual('unknown', empty_name_node.name)
        self.assertEqual(50, empty_name_node.value)

    def test_aggregation_sorting(self):
        # Check all cumulative values and the order.
        view = MemoryUsageView.from_json(_REALISTIC_JSON)
        view.roots = _aggregate_nodes(view.roots)
        self.assertEqual(1, len(view.roots))
        root_node = view.roots[0]
        self.assertEqual('main', root_node.name)
        self.assertEqual(48 + 16 + 8, root_node.value)
        self.assertEqual(3, len(root_node.children))
        bar_node, malloc_node, foo_node = root_node.children
        self.assertEqual('bar', bar_node.name)
        self.assertEqual(48, bar_node.value)
        self.assertEqual('malloc', malloc_node.name)
        self.assertEqual(16, malloc_node.value)
        self.assertEqual('foo', foo_node.name)
        self.assertEqual(8, foo_node.value)

    def test_aggregation_sorting2(self):
        # Change the order of the children and verify that
        # it leads to the same order after aggregation.
        view = MemoryUsageView.from_json(_REALISTIC_JSON)
        foo_node, malloc_node, bar_node = view.roots[0].children
        view.roots[0].children = [foo_node, bar_node, malloc_node]
        view.roots = _aggregate_nodes(view.roots)
        root_node = view.roots[0]
        bar_node, malloc_node, foo_node = root_node.children
        self.assertEqual('bar', bar_node.name)
        self.assertEqual(48, bar_node.value)
        self.assertEqual('malloc', malloc_node.name)
        self.assertEqual(16, malloc_node.value)
        self.assertEqual('foo', foo_node.name)
        self.assertEqual(8, foo_node.value)

    def test_aggregation_merge_duplicate_names(self):
        view = MemoryUsageView.from_json(_REALISTIC_JSON)
        # Make two adjacent node names match for merging.
        foo_node, _, bar_node = view.roots[0].children
        self.assertEqual('bar', bar_node.name)
        foo_node.name = 'bar'
        view.roots = _aggregate_nodes(view.roots)
        self.assertEqual(1, len(view.roots))
        root_node = view.roots[0]
        self.assertEqual(2, len(root_node.children))
        bar_node, malloc_node = root_node.children
        self.assertEqual('bar', bar_node.name)
        self.assertEqual(48 + 8, bar_node.value)
        self.assertEqual('malloc', malloc_node.name)
        self.assertEqual(16, malloc_node.value)

    def test_zip_by_name_both_empty(self):
        self.assertEqual([], list(_zip_by_name([], [])))

    def test_zip_by_name_one_empty(self):
        node = TreeNode(name='abc')
        self.assertEqual([(node, None)], list(_zip_by_name([node], [])))
        self.assertEqual([(None, node)], list(_zip_by_name([], [node])))
        node2 = TreeNode(name='def')
        self.assertEqual(
            [(None, node), (None, node2)], list(_zip_by_name([], [node, node2]))
        )

    def test_zip_by_name_simple(self):
        left_node1 = TreeNode(name='l1')
        left_node2 = TreeNode(name='l2')
        left_nodes = [left_node1, left_node2]
        right_node1 = TreeNode(name='r1')
        right_node2 = TreeNode(name='r2')
        right_nodes = [right_node1, right_node2]

        result = list(_zip_by_name(left_nodes, right_nodes))

        self.assertEqual(4, len(result))
        self.assertIn((left_node1, None), result)
        self.assertIn((left_node2, None), result)
        self.assertIn((None, right_node1), result)
        self.assertIn((None, right_node2), result)

    def test_zip_by_name_with_intersection(self):
        left_node1 = TreeNode(name='l1')
        left_abc = TreeNode(name='abc')
        left_nodes = [left_node1, left_abc]
        right_abc = TreeNode(name='abc')
        right_node2 = TreeNode(name='r2')
        right_nodes = [right_abc, right_node2]

        result = list(_zip_by_name(left_nodes, right_nodes))

        self.assertEqual(3, len(result))
        self.assertIn((left_abc, right_abc), result)
        self.assertIn((left_node1, None), result)
        self.assertIn((None, right_node2), result)

    def test_compare_nothing(self):
        self.assertEqual([], _compare_node_lists([], []))

    def test_compare_one_node_to_nothing(self):
        node = TreeNode(name='one', value=123)
        result_node = TreeNode(name='one', value=123, delta=-123)

        self.assertEqual([result_node], _compare_node_lists([node], []))

        node.value = 456
        result_node.value = 0
        result_node.delta = 456

        self.assertEqual([result_node], _compare_node_lists([], [node]))

    def test_compare_same_name_nodes(self):
        node_base = TreeNode(name='pikachu', value=123)
        node_new = TreeNode(name='pikachu', value=456)

        result = _compare_node_lists([node_base], [node_new])
        self.assertEqual(1, len(result))
        result_node = result[0]
        self.assertEqual(123, result_node.value)
        self.assertEqual('pikachu', result_node.name)
        self.assertEqual(456 - 123, result_node.delta)

    def test_compare_children(self):
        base_root = TreeNode(name='root', value=30)
        new_root = TreeNode(name='root', value=80)
        base_root.children = [
            TreeNode(name='common_child', value=10),
            TreeNode(name='base_only_child', value=20),
        ]
        new_root.children = [
            TreeNode(name='common_child', value=30),
            TreeNode(name='new_only_child', value=50),
        ]

        result_nodes = _compare_node_lists([base_root], [new_root])
        self.assertEqual(1, len(result_nodes))
        result_root = result_nodes[0]

        self.assertEqual('root', result_root.name)
        self.assertEqual(30, result_root.value)
        self.assertEqual(80 - 30, result_root.delta)

        self.assertEqual(3, len(result_root.children))
        new_only, common_child, base_only = result_root.children

        self.assertEqual('new_only_child', new_only.name)
        self.assertEqual(0, new_only.value)
        self.assertEqual(50, new_only.delta)

        self.assertEqual('common_child', common_child.name)
        self.assertEqual(10, common_child.value)
        self.assertEqual(30 - 10, common_child.delta)

        self.assertEqual('base_only_child', base_only.name)
        self.assertEqual(20, base_only.value)
        self.assertEqual(-20, base_only.delta)

    def test_compare_children_and_single(self):
        base_root = TreeNode(name='root', value=30)
        new_root = TreeNode(name='root', value=80)
        base_root.children = [
            TreeNode(name='child10', value=10),
            TreeNode(name='child20', value=20),
        ]
        result_nodes = _compare_node_lists([base_root], [new_root])
        self.assertEqual(1, len(result_nodes))
        result_root = result_nodes[0]

        self.assertEqual('root', result_root.name)
        self.assertEqual(30, result_root.value)
        self.assertEqual(80 - 30, result_root.delta)

        self.assertEqual(2, len(result_root.children))
        child_10, child_20 = result_root.children
        self.assertEqual(10, child_10.value)
        self.assertEqual(-10, child_10.delta)
        self.assertEqual(20, child_20.value)
        self.assertEqual(-20, child_20.delta)

    def test_prettify_size(self):
        tests = [
            (500, 500),
            (1024, '1 KiB'),
            (1536, '1.5 KiB'),
            (1500, '1.46 KiB'),
            (1048576, '1 MiB'),
            (123456789, '117.74 MiB'),
            (1099511627776, '1024 GiB'),
            (2000000000000, 2000000000000),
        ]

        test_lines = []
        for inp, expected in tests:
            self.assertEqual(expected, _prettify_size(inp))


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