# MIT License
#
# Copyright (c) 2023-2025 Advanced Micro Devices, Inc. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from __future__ import absolute_import


def test_perfetto_data(
    pftrace_data,
    json_data,
    categories=(
        "hip",
        "hsa",
        "marker",
        "kernel",
        "memory_copy",
        "memory_allocation",
        "rocdecode_api",
        "rocjpeg_api",
        "counter_collection",
        "scratch_memory",
    ),
):

    mapping = {
        "hip": ("hip_api", "hip_api"),
        "hsa": ("hsa_api", "hsa_api"),
        "marker": ("marker_api", "marker_api"),
        "kernel": ("kernel_dispatch", "kernel_dispatch"),
        "memory_copy": ("memory_copy", "memory_copy"),
        "memory_allocation": ("memory_allocation", "memory_allocation"),
        "rocdecode_api": ("rocdecode_api", "rocdecode_api"),
        "rocjpeg_api": ("rocjpeg_api", "rocjpeg_api"),
        "counter_collection": ("counter_collection", "counter_collection"),
        "scratch_memory": ("scratch_memory", "scratch_memory"),
    }

    # make sure they specified valid categories
    for itr in categories:
        assert itr in mapping.keys()

    for pf_category, js_category in [
        itr for key, itr in mapping.items() if key in categories
    ]:
        _pf_data = pftrace_data.loc[pftrace_data["category"] == pf_category]

        _js_data = []
        if js_category != "counter_collection":
            _js_data = json_data["rocprofiler-sdk-tool"]["buffer_records"][js_category]
        else:
            unique_counter_ids = set()

            counter_info = {}
            for itr in json_data["rocprofiler-sdk-tool"]["counters"]:
                counter_info[itr["id"]["handle"]] = itr

            agent_info = {}
            for itr in json_data["rocprofiler-sdk-tool"]["agents"]:
                agent_info[itr["id"]["handle"]] = itr

            for dispatch_entry in json_data["rocprofiler-sdk-tool"]["callback_records"][
                js_category
            ]:
                counter_records = dispatch_entry["records"]
                agent_id = dispatch_entry["dispatch_data"]["dispatch_info"]["agent_id"][
                    "handle"
                ]
                agent_node_id = agent_info[agent_id]["node_id"]

                for record in counter_records:
                    counter_id = record["counter_id"]["handle"]
                    counter_name = counter_info[counter_id]["name"]
                    unique_counter_ids.add(f"Agent [{agent_node_id}] PMC {counter_name}")

            _js_data = [{"counter_id": id} for id in unique_counter_ids]

        assert len(_pf_data) == len(
            _js_data
        ), f"{pf_category} ({len(_pf_data)}):\n\t{_pf_data}\n{js_category} ({len(_js_data)}):\n\t{_js_data}"


def test_otf2_data(
    otf2_data, json_data, categories=("hip", "hsa", "marker", "kernel", "memory_copy")
):
    def get_operation_name(kind_id, op_id):
        return json_data["rocprofiler-sdk-tool"]["strings"]["buffer_records"][kind_id][
            "operations"
        ][op_id]

    def get_kind_name(kind_id):
        return json_data["rocprofiler-sdk-tool"]["strings"]["buffer_records"][kind_id][
            "kind"
        ]

    mapping = {
        "hip": ("hip_api", "hip_api"),
        "hsa": ("hsa_api", "hsa_api"),
        "marker": ("marker_api", "marker_api"),
        "kernel": ("kernel_dispatch", "kernel_dispatch"),
        "memory_copy": ("memory_copy", "memory_copy"),
        "memory_allocation": ("memory_allocation", "memory_allocation"),
        "rocdecode_api": ("rocdecode_api", "rocdecode_api"),
        "rocjpeg_api": ("rocjpeg_api", "rocjpeg_api"),
    }

    # make sure they specified valid categories
    for itr in categories:
        assert itr in mapping.keys()

    for otf2_category, json_category in [
        itr for key, itr in mapping.items() if key in categories
    ]:
        _otf2_data = otf2_data.loc[otf2_data["category"] == otf2_category]
        _json_data = json_data["rocprofiler-sdk-tool"]["buffer_records"][json_category]

        # we do not encode the roctxMark "regions" in OTF2 because
        # they don't map to the OTF2_REGION_ROLE_FUNCTION well
        if json_category == "marker_api":

            def roctx_mark_filter(val):
                return (
                    None
                    if get_kind_name(val.kind)
                    in ["MARKER_CORE_API", "MARKER_CORE_RANGE_API"]
                    and get_operation_name(val.kind, val.operation) == "roctxMarkA"
                    else val
                )

            _json_data = [itr for itr in _json_data if roctx_mark_filter(itr) is not None]

        assert len(_otf2_data) == len(
            _json_data
        ), f"{otf2_category} ({len(_otf2_data)}):\n\t{_otf2_data}\n{json_category} ({len(_json_data)}):\n\t{_json_data}"


def test_otf2_system_tree_node(otf2_data):
    """
    Validate that each system tree node has class_name with AMD in it, and has ACCELERATOR_DEVICE domain
    Refer to https://github.com/ROCm/rocm-systems/pull/2366 for history
    """
    import otf2

    # Build map of system_tree_node to system_tree_node_domain
    unique_nodes = otf2_data.drop_duplicates(subset=["system_tree_node"])
    node_name_and_domain_map = [
        {
            "name": row["system_tree_node"].name,
            "class_name": row["system_tree_node"].class_name,
            "domain": row["system_tree_node_domain"],
        }
        for _, row in unique_nodes.iterrows()
    ]
    print("\n")

    # Now check the system_tree_node_domain is correctly set to ACCELERATOR_DEVICE
    count = 0
    for node in node_name_and_domain_map:
        if "AMD" in node["class_name"]:
            if node["domain"] == otf2.SystemTreeDomain.ACCELERATOR_DEVICE:
                print(
                    f"MATCHED - SystemTreeNode {node['name']} with class {node['class_name']} had system_tree_node_domain: {node['domain']}"
                )
                count += 1
            else:
                assert (
                    node["domain"] == otf2.SystemTreeDomain.ACCELERATOR_DEVICE
                ), f"SystemTreeNode {node['name']} with class {node['class_name']} validation failed: domain is {node['domain']}, expected 'ACCELERATOR_DEVICE'"

    # Each OTF2 file should have at least 1 node with SystemTreeNodeDomain == ACCELERATOR_DEVICE
    assert count > 0, f"No ACCELERATOR_DEVICE nodes found in OTF2 file\n"


def test_rocpd_data(
    rocpd_data,
    json_data,
    categories=(
        "hip",
        "hsa",
        "marker",
        "kernel",
        "memory_copy",
        "memory_allocation",
        "rocdecode_api",
        "rocjpeg_api",
    ),
):

    mapping = {
        "hip": (
            "hip_api",
            (
                "HIP_COMPILER_API",
                "HIP_COMPILER_API_EXT",
                "HIP_RUNTIME_API",
                "HIP_RUNTIME_API_EXT",
            ),
        ),
        "hsa": (
            "hsa_api",
            (
                "HSA_CORE_API",
                "HSA_AMD_EXT_API",
                "HSA_IMAGE_EXT_API",
                "HSA_FINALIZE_EXT_API",
            ),
        ),
        "marker": (
            "marker_api",
            (
                "MARKER_CORE_API",
                "MARKER_CONTROL_API",
                "MARKER_NAME_API",
                "MARKER_CORE_RANGE_API",
            ),
        ),
        "kernel": ("kernel_dispatch", ("KERNEL_DISPATCH")),
        "memory_copy": ("memory_copy", ("MEMORY_COPY")),
        "memory_allocation": ("memory_allocation", ("MEMORY_ALLOCATION")),
        "rocdecode_api": ("rocdecode_api", ("ROCDECODE_API")),
        "rocjpeg_api": ("rocjpeg_api", ("ROCJPEG_API")),
    }

    view_mapping = {
        "hip_api": "regions",
        "hsa_api": "regions",
        "marker_api": "regions_and_samples",
        "rccl_api": "regions",
        "rocdecode_api": "regions",
        "rocjpeg_api": "regions",
        "kernel_dispatch": "kernels",
        "memory_copy": "memory_copies",
        "memory_allocation": "memory_allocations",
    }

    # make sure they specified valid categories
    for itr in categories:
        assert itr in mapping.keys()

    for js_category, rpd_category in [
        itr for key, itr in mapping.items() if key in categories
    ]:
        _js_data = json_data["rocprofiler-sdk-tool"]["buffer_records"][js_category]
        _rpd_cats = (
            rpd_category if isinstance(rpd_category, (list, tuple)) else [rpd_category]
        )
        _rpd_cond = " OR ".join([f"category = '{itr}'" for itr in _rpd_cats])
        _rpd_query = f"SELECT * FROM {view_mapping[js_category]} WHERE {_rpd_cond}"
        _rpd_data = rocpd_data.execute(_rpd_query).fetchall()

        assert len(_rpd_data) == len(
            _js_data
        ), f"query: {_rpd_query}\n{rpd_category} ({len(_rpd_data)}):\n\t{_rpd_data}\n{js_category} ({len(_js_data)}):\n\t{_js_data}"

    # if duplicate entries exist from double buffering synchronization issues, there will be duplicate start and end times
    for itr in ["regions", "kernels", "memory_copies", "memory_allocations"]:
        _num_rpd_tot = rocpd_data.execute(f"SELECT COUNT(*) FROM {itr}").fetchone()[0]
        _num_rpd_start = rocpd_data.execute(
            f"SELECT COUNT(DISTINCT(start)) FROM {itr}"
        ).fetchone()[0]
        _num_rpd_end = rocpd_data.execute(
            f"SELECT COUNT(DISTINCT(end)) FROM {itr}"
        ).fetchone()[0]

        assert _num_rpd_tot == _num_rpd_start == _num_rpd_end, (
            f"Duplicate records check failed for {itr}: total {itr}={_num_rpd_tot}, "
            f"unique starts={_num_rpd_start}, unique ends={_num_rpd_end}. In rocprofv3, "
            "this likely means the double buffering scheme updated a buffer with new "
            "records while it was being processed in a buffer flush"
        )


def _perform_time_sanity_checks(data):
    """Helper function to perform time sanity checks on data."""
    columns = data[0].keys()
    start_columns = [c for c in columns if "start" in c.lower() and "time" in c.lower()]
    end_columns = [c for c in columns if "end" in c.lower() and "time" in c.lower()]

    if not start_columns or not end_columns:
        return None, None

    for record in data:
        start_time = record[start_columns[0]]
        end_time = record[end_columns[0]]
        assert int(start_time) >= 0, f"Time error: Start time ({start_time}) < 0)."
        assert int(end_time) >= 0, f"Time error: End time ({end_time}) < 0)."
        assert int(end_time) >= int(
            start_time
        ), f"Time error: End time ({end_time}) < Start time ({start_time})."

    return start_columns[0], end_columns[0]


def _perform_csv_json_match(csv_row, json_row, mapping, json_data):

    def get_nested(d, path):
        """Helper to get nested dict values using dot notation."""
        keys = path.split(".")
        for k in keys:
            if isinstance(d, dict):
                d = d.get(k)
            else:
                return None
        return d

    for csv_key, json_info in mapping.items():
        if json_info is None:
            continue

        csv_value = csv_row[csv_key]

        if csv_key == "Operation":
            json_value = json_data["rocprofiler-sdk-tool"]["strings"]["buffer_records"][
                json_row["kind"]
            ]["operations"][json_row["operation"]]

            assert str(csv_value) in str(
                json_value
            ), f"Mismatch for {csv_key}: CSV={csv_value} JSON={json_value}"
            continue

        if csv_key == "Function":
            json_value = json_data["rocprofiler-sdk-tool"]["strings"]["buffer_records"][
                json_row["kind"]
            ]["operations"][json_row["operation"]]
        else:
            json_path, subkey = json_info
            json_value = get_nested(json_row, json_path)
            if subkey:
                json_value = (
                    json_value.get(subkey) if isinstance(json_value, dict) else None
                )

        assert str(csv_value) == str(
            json_value
        ), f"Mismatch for {csv_key}: CSV={csv_value} JSON={json_value}"


def test_csv_data(
    csv_data,
    json_data,
    categories=(
        "agent",
        "counter_collection",
        "kernel",
        "memory_allocation",
        "memory_copy",
        "regions",
    ),
):

    mapping = {
        "counter_collection": "counter_collection",
        "kernel": "kernel_dispatch",
        "memory_allocation": "memory_allocation",
        "memory_copy": "memory_copy",
    }

    keys_mapping = {
        "kernel": {
            "Thread_Id": ("thread_id", None),
            "Correlation_Id": ("correlation_id", "internal"),
            "Start_Timestamp": ("start_timestamp", None),
            "End_Timestamp": ("end_timestamp", None),
            "Queue_Id": ("dispatch_info.queue_id.handle", None),
            "Kernel_Id": ("dispatch_info.kernel_id", None),
            "Dispatch_Id": ("dispatch_info.dispatch_id", None),
            "Stream_Id": ("stream_id.handle", None),
            "Workgroup_Size_X": ("dispatch_info.workgroup_size.x", None),
            "Workgroup_Size_Y": ("dispatch_info.workgroup_size.y", None),
            "Workgroup_Size_Z": ("dispatch_info.workgroup_size.z", None),
            "Grid_Size_X": ("dispatch_info.grid_size.x", None),
            "Grid_Size_Y": ("dispatch_info.grid_size.y", None),
            "Grid_Size_Z": ("dispatch_info.grid_size.z", None),
        },
        "memory_allocation": {
            "Operation": (),  # Special case
            "Correlation_Id": ("correlation_id", "internal"),
            "Start_Timestamp": ("start_timestamp", None),
            "End_Timestamp": ("end_timestamp", None),
        },
        "memory_copy": {
            "Correlation_Id": ("correlation_id", "internal"),
            "Start_Timestamp": ("start_timestamp", None),
            "End_Timestamp": ("end_timestamp", None),
        },
        "regions": {
            "Thread_Id": ("thread_id", None),
            "Correlation_Id": ("correlation_id", "internal"),
            "Start_Timestamp": ("start_timestamp", None),
            "End_Timestamp": ("end_timestamp", None),
        },
    }

    for data in csv_data:
        filename, _csv_data = data
        file_category = [category for category in categories if category in filename]
        assert len(file_category) > 0, f"{filename} is not a valid csv filename"
        category = file_category[0]

        if category == "counter_collection":
            _js_data = json_data["rocprofiler-sdk-tool"]["callback_records"][category]
        elif category == "agent":
            _js_data = json_data["rocprofiler-sdk-tool"]["agents"]
        elif category == "regions":
            buffer_records = json_data["rocprofiler-sdk-tool"]["buffer_records"]
            _js_data = []
            for key, value in buffer_records.items():
                if key == "marker_api":
                    string_records = []
                    marker_records = json_data["rocprofiler-sdk-tool"]["buffer_records"][
                        "marker_api"
                    ]
                    for item in json_data["rocprofiler-sdk-tool"]["strings"][
                        "buffer_records"
                    ]:
                        if item["kind"] == "MARKER_CORE_RANGE_API":
                            string_records = item["operations"]
                    exclude_ops = {"roctxGetThreadId"}
                    for entry in marker_records:
                        # exclude records where start and end times are the same
                        if entry["start_timestamp"] == entry["end_timestamp"]:
                            continue
                        # excludes roctxMarkA and roctxGetThreadId operations
                        if (
                            string_records
                            and string_records[entry["operation"]] not in exclude_ops
                        ):
                            _js_data.append(entry)
                elif key == "kfd":
                    kfd_records = json_data["rocprofiler-sdk-tool"]["buffer_records"][
                        "kfd"
                    ]
                    for entry in kfd_records:
                        # for instantaneous records, add start_timestamp/end_timestamp to the json data
                        if "timestamp" in entry:
                            entry["start_timestamp"] = entry["timestamp"]
                            entry["end_timestamp"] = entry["timestamp"]

                        # report pid as thread_id to match CSV
                        entry["thread_id"] = entry["pid"]

                        # report 0 correlation ID
                        entry["correlation_id"] = {
                            "internal": 0,
                            "external": 0,
                            "ancestor": 0,
                        }

                        _js_data.append(entry)
                else:
                    if key.endswith("_api"):
                        _js_data.extend(value)
        else:
            json_records_key = mapping[category]
            _js_data = json_data["rocprofiler-sdk-tool"]["buffer_records"][
                json_records_key
            ]

        assert len(_js_data) == len(
            _csv_data
        ), f"Size mismatch for {category}: JSON size= {len(_js_data)} rows, CSV size= {len(_csv_data)} rows."

        if not _csv_data:
            continue  # Exit if there is no data to validate

        csv_start_col, csv_end_col = _perform_time_sanity_checks(_csv_data)
        json_start_col, json_end_col = _perform_time_sanity_checks(_js_data)

        if None in (csv_start_col, json_start_col, csv_end_col, json_end_col):
            continue

        # Helper to get correlation_id for tiebreaking when timestamps are identical
        def get_csv_corr_id(x):
            return int(x.get("Correlation_Id", 0))

        def get_json_corr_id(x):
            corr = x.get("correlation_id", {})
            if isinstance(corr, dict):
                return int(corr.get("internal", 0))
            return int(corr) if corr else 0

        _csv_data_sorted = sorted(
            _csv_data,
            key=lambda x: (
                int(x[csv_start_col]),
                int(x[csv_end_col]),
                get_csv_corr_id(x),
            ),
        )
        _js_data_sorted = sorted(
            _js_data,
            key=lambda x: (
                int(x[json_start_col]),
                int(x[json_end_col]),
                get_json_corr_id(x),
            ),
        )

        for a, b in zip(_csv_data_sorted, _js_data_sorted):
            _perform_csv_json_match(a, b, keys_mapping[category], json_data)


def test_summary_region_category_filtering(
    summary_dir, expected_categories=None, allow_none=False
):
    """
    Test that summary output contains ONLY the expected categories.

    Args:
        summary_dir: Path to directory containing summary CSV files
        expected_categories: List of category names that should be present (e.g., ['kernel', 'hip'])
        allow_none: If True, allows no region summaries (for --region-categories NONE test)
    """
    import os
    import glob

    if not os.path.exists(summary_dir):
        raise FileNotFoundError(f"Summary directory not found: {summary_dir}")

    # Get all CSV files
    csv_files = glob.glob(os.path.join(summary_dir, "*.csv"))
    basenames = [os.path.basename(f) for f in csv_files]

    assert len(basenames) > 0, f"No summary files found in {summary_dir}"

    print(f"\nFound {len(basenames)} summary files in {summary_dir}:")
    for name in sorted(basenames):
        print(f"  - {name}")

    # For NONE test: ensure no region-based summaries are generated
    if allow_none:
        # Region summaries have filenames like "rocm_hip_*.csv", "rocm_hsa_*.csv"
        region_files = [f for f in basenames if f.lower().startswith("rocm_")]
        assert len(region_files) == 0, (
            f"--region-categories NONE should not generate region summaries, "
            f"but found: {region_files}"
        )

    # Check that expected categories are present and ONLY those categories exist
    if expected_categories:
        # 1. Check all expected categories are present
        for category in expected_categories:
            category_lower = category.lower()
            matching_files = [f for f in basenames if category_lower in f.lower()]
            assert len(matching_files) > 0, (
                f"Expected category '{category}' not found. "
                f"No files matching '{category_lower}' in {basenames}"
            )

        # 2. Check no unexpected categories exist
        for filename in basenames:
            filename_lower = filename.lower()
            matches_expected = any(
                cat.lower() in filename_lower for cat in expected_categories
            )
            assert matches_expected, (
                f"Unexpected file '{filename}' found. "
                f"Does not match any expected category: {expected_categories}"
            )


def test_perfetto_arg_annotations(pftrace_reader):
    """
    Test that function argument annotations are available in perfetto with --annotate-args.
    """
    # Query for API function argument annotations from --annotate-args
    # Filter for hip_api/hsa_api/marker_api (KFD/kernel have args from other sources)
    # Exclude metadata fields (always present, even without --annotate-args)
    query = """
    SELECT
        slice.name as slice_name,
        slice.category as slice_category,
        slice.id as slice_id,
        args.key as arg_name,
        args.string_value as arg_value
    FROM slice
    JOIN args ON slice.arg_set_id = args.arg_set_id
    WHERE args.key LIKE 'debug.%'
      AND slice.category IN ('hip_api', 'hsa_api', 'marker_api')
      AND args.key NOT IN (
        'debug.begin_ns', 'debug.end_ns', 'debug.delta_ns',
        'debug.tid', 'debug.kind', 'debug.operation',
        'debug.corr_id', 'debug.ancestor_id'
      )
    """

    result = pftrace_reader.query_tp(query)

    # Function argument annotations must exist - perfetto was generated with --annotate-args
    assert (
        not result.empty
    ), "Expected function argument annotations not found - --annotate-args may be broken. "


def test_perfetto_event_id_annotations(pftrace_reader):
    """
    Test that hipEvent operations have properly annotated event IDs.
    """
    # Query for all hipEvent operations with their event annotations
    event_ops_query = """
    SELECT
        slice.name as operation,
        args.string_value as event_id,
        slice.ts as timestamp
    FROM slice
    JOIN args ON slice.arg_set_id = args.arg_set_id
    WHERE slice.name IN ('hipEventCreate', 'hipEventRecord', 'hipEventDestroy')
      AND args.key = 'debug.event'
    ORDER BY timestamp
    """

    event_ops = pftrace_reader.query_tp(event_ops_query)

    # Validate we have event data
    assert not event_ops.empty, (
        "No hipEvent operations found with event ID annotations. "
        "Ensure --annotate-args is enabled and the test app uses HIP events."
    )

    # Group by operation type
    creates = event_ops[event_ops["operation"] == "hipEventCreate"]
    records = event_ops[event_ops["operation"] == "hipEventRecord"]
    destroys = event_ops[event_ops["operation"] == "hipEventDestroy"]

    # Get unique event IDs for each operation
    created_ids = set(creates["event_id"].unique())
    recorded_ids = set(records["event_id"].unique())
    destroyed_ids = set(destroys["event_id"].unique())

    # Validate counts
    num_creates = len(creates)
    num_records = len(records)
    num_destroys = len(destroys)
    num_unique_created = len(created_ids)
    num_unique_recorded = len(recorded_ids)
    num_unique_destroyed = len(destroyed_ids)

    assert num_creates > 0, "No hipEventCreate operations found"
    assert num_records > 0, "No hipEventRecord operations found"
    assert num_destroys > 0, "No hipEventDestroy operations found"

    # hipEventRecord and hipEventDestroy receive event by value, so IDs should match
    assert recorded_ids == destroyed_ids, (
        f"Mismatch between recorded and destroyed event IDs.\n"
        f"Recorded but not destroyed: {recorded_ids - destroyed_ids}\n"
        f"Destroyed but not recorded: {destroyed_ids - recorded_ids}"
    )

    # NOTE: hipEventCreate annotations will show pointer addresses captured on API entry before being initialized, not event handles.
    # Therefore, we cannot validate created_ids == recorded_ids.

    # Each event should be created exactly once
    create_counts = creates["event_id"].value_counts()
    multiple_creates = create_counts[create_counts > 1]
    assert multiple_creates.empty, (
        f"Found {len(multiple_creates)} event(s) created multiple times: "
        f"{dict(multiple_creates)}"
    )

    # Each event should be destroyed exactly once
    destroy_counts = destroys["event_id"].value_counts()
    multiple_destroys = destroy_counts[destroy_counts > 1]
    assert multiple_destroys.empty, (
        f"Found {len(multiple_destroys)} event(s) destroyed multiple times: "
        f"{dict(multiple_destroys)}"
    )

    # Validate expected counts based on test parameters
    expected_creates = 4  # 2 threads × 2 events per thread
    expected_records = 2000  # 2 threads × 2 events × 500 iterations
    expected_destroys = 4  # 2 threads × 2 events per thread

    assert (
        num_creates == expected_creates
    ), f"Expected {expected_creates} hipEventCreate calls but found {num_creates}"

    assert (
        num_records == expected_records
    ), f"Expected {expected_records} hipEventRecord calls but found {num_records}"

    assert (
        num_destroys == expected_destroys
    ), f"Expected {expected_destroys} hipEventDestroy calls but found {num_destroys}"
