#!/usr/bin/env python3
# MIT License
#
# Copyright (c) 2024-2025 Advanced Micro Devices, Inc.
#
# 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.

import sys

import pytest


def extract_json_kernel_records(json_root):
    """Extract kernel dispatch records from JSON output."""
    assert "rocprofiler-sdk-tool" in json_root, "missing rocprofiler-sdk-tool in JSON"
    tool = json_root["rocprofiler-sdk-tool"]
    if isinstance(tool, list) and len(tool) > 0:
        tool = tool[0]

    assert "buffer_records" in tool, "missing buffer_records in JSON"
    buffer_records = tool["buffer_records"]

    for key in ("kernel_dispatch", "kernel_trace", "kernel_dispatch_trace"):
        if (
            key in buffer_records
            and isinstance(buffer_records[key], list)
            and len(buffer_records[key]) > 0
        ):
            return buffer_records[key]

    raise AssertionError(
        "no kernel dispatch records found in JSON buffer_records keys="
        f"{list(buffer_records.keys())}"
    )


def _as_int(value, *, field="value"):
    assert value is not None, f"missing {field}"
    try:
        return int(value)
    except Exception as exc:
        raise AssertionError(
            f"failed to parse int for {field}: {value!r} ({exc})"
        ) from exc


def _extract_dispatch_id_from_json_record(record):
    """
    Prefer dispatch_info.dispatch_id.
    Fallback to correlation_id.internal (or correlation_id if scalar).
    Return int.
    """
    dispatch_info = record.get("dispatch_info", {})
    dispatch_id = None

    if isinstance(dispatch_info, dict):
        dispatch_id = dispatch_info.get("dispatch_id", None)

    if dispatch_id is None:
        correlation_id = record.get("correlation_id", {})
        if isinstance(correlation_id, dict):
            dispatch_id = correlation_id.get("internal", None)
        else:
            dispatch_id = correlation_id

    return _as_int(dispatch_id, field="dispatch_id/correlation_id")


def build_json_duration_map(records):
    """
    Build map:
        key(int dispatch_id) -> (start, end, duration)
    """
    result = {}

    for record in records:
        dispatch_id = _extract_dispatch_id_from_json_record(record)

        start = _as_int(record.get("start_timestamp"), field="start_timestamp")
        end = _as_int(record.get("end_timestamp"), field="end_timestamp")

        assert start > 0 and end > 0, f"invalid timestamps start={start} end={end}"
        assert end >= start, f"end before start: start={start} end={end}"

        assert (
            dispatch_id not in result
        ), f"duplicate dispatch_id {dispatch_id} found in JSON records"
        result[dispatch_id] = (start, end, end - start)

    assert len(result) > 0, "no kernel records extracted from JSON"
    return result


def load_kernel_rows_via_rocpd(db_path):
    """
    Use rocpd Python API and reuse rocpd.csv kernel query logic directly.
    Returns list[dict].
    """
    try:
        import rocpd
        from rocpd import csv as rocpd_csv
        from rocpd import output_config as rocpd_output_config
    except Exception as exc:
        raise AssertionError(
            "failed to import rocpd python modules. "
            f"Ensure PYTHONPATH is set for rocprofiler-sdk build tree. ({exc})"
        ) from exc

    data = rocpd.connect([db_path])

    config = rocpd_output_config.output_config()
    query = rocpd_csv.get_kernel_csv_query(config)

    cursor = rocpd.execute(data, query)
    columns = [desc[0] for desc in cursor.description]
    rows = [dict(zip(columns, row)) for row in cursor.fetchall()]

    assert len(rows) > 0, f"no rows returned from kernel query in db: {db_path}"
    return rows


_DB_ROW_COLUMN_NAMES = ["dispatch_id", "start_timestamp", "end_timestamp", "duration"]


def _extract_values_from_db_row(row):
    """
    Extract (dispatch_id, start, end, duration) from a DB row dict.
    Builds a single case-insensitive lookup table for the row, then fetches
    each required column. Column names reflect the output of
    rocpd_csv.get_kernel_csv_query(), which aliases and extends the raw
    rocpd_kernel_dispatch schema columns.
    """
    lowered_row = {k.lower(): v for k, v in row.items()}
    dispatch_id, start, end, duration = [
        _as_int(lowered_row.get(name), field=name) for name in _DB_ROW_COLUMN_NAMES
    ]
    return dispatch_id, start, end, duration


def test_rocpd_kernel_trace_duration(json_data, db_path):
    """
    Validate that kernel trace Duration exists on the rocpd CSV path and matches JSON.

    Strategy:
      - JSON and rocpd DB are generated from the SAME rocprofv3 execution
        (ROCPROF_OUTPUT_FORMAT=json,rocpd)
      - Read kernel records from JSON
      - Read kernel rows from rocpd DB using rocpd Python API
      - Reuse rocpd.csv.get_kernel_csv_query(config) so test and CSV path stay aligned
      - Enforce:
          * Duration == End_Timestamp - Start_Timestamp
          * Start/End/Duration EXACTLY match JSON for each dispatch_id
          * All DB rows match JSON rows
    """
    db_rows = load_kernel_rows_via_rocpd(db_path)

    json_records = extract_json_kernel_records(json_data)
    json_map = build_json_duration_map(json_records)

    total_count = len(db_rows)
    matched_count = 0
    mismatches = []
    missing_in_json = []

    for row in db_rows:
        dispatch_id, start, end, duration = _extract_values_from_db_row(row)

        assert (
            start > 0 and end > 0
        ), f"invalid DB timestamps: start={start} end={end} dispatch_id={dispatch_id}"
        assert (
            end >= start
        ), f"DB end before start: start={start} end={end} dispatch_id={dispatch_id}"
        assert duration == (end - start), (
            f"DB duration mismatch: duration={duration} != end-start={end - start} "
            f"dispatch_id={dispatch_id}"
        )

        if dispatch_id not in json_map:
            missing_in_json.append(dispatch_id)
            continue

        matched_count += 1
        json_start, json_end, json_duration = json_map[dispatch_id]

        start_diff = start - json_start
        end_diff = end - json_end
        duration_diff = duration - json_duration

        if start_diff != 0 or end_diff != 0 or duration_diff != 0:
            mismatches.append(
                {
                    "dispatch_id": dispatch_id,
                    "db_start": start,
                    "json_start": json_start,
                    "start_diff": start_diff,
                    "db_end": end,
                    "json_end": json_end,
                    "end_diff": end_diff,
                    "db_duration": duration,
                    "json_duration": json_duration,
                    "duration_diff": duration_diff,
                }
            )

    if missing_in_json:
        sample = missing_in_json[:10]
        raise AssertionError(
            "Some DB kernel rows had dispatch_id not present in JSON records. "
            "Since JSON and rocpd come from the same execution, dispatch IDs should align.\n"
            f"Missing count: {len(missing_in_json)}/{total_count}\n"
            f"Sample missing dispatch_ids: {sample}"
        )

    if mismatches:
        lines = [
            "",
            "TIMESTAMP/DURATION MISMATCHES DETECTED",
            f"{'Dispatch':<12} {'StartDiff':<12} {'EndDiff':<12} {'DurDiff':<12}",
            "=" * 56,
        ]

        for item in mismatches[:10]:
            lines.append(
                f"{item['dispatch_id']:<12} "
                f"{item['start_diff']:<12} "
                f"{item['end_diff']:<12} "
                f"{item['duration_diff']:<12}"
            )

        if len(mismatches) > 10:
            lines.append(f"... and {len(mismatches) - 10} more mismatches")

        first = mismatches[0]
        detail = (
            f"\n\nExample mismatch for dispatch {first['dispatch_id']}:\n"
            f"  DB:   start={first['db_start']}, end={first['db_end']}, "
            f"duration={first['db_duration']}\n"
            f"  JSON: start={first['json_start']}, end={first['json_end']}, "
            f"duration={first['json_duration']}\n"
            f"  Diff: start={first['start_diff']}, end={first['end_diff']}, "
            f"duration={first['duration_diff']}\n"
            f"Total mismatches: {len(mismatches)}/{total_count}\n"
            "NOTE: Since JSON and rocpd come from the same execution, these should be identical."
        )
        raise AssertionError("\n".join(lines) + detail)

    assert matched_count > 0, "No DB rows matched JSON records"
    assert (
        matched_count == total_count
    ), f"Only {matched_count}/{total_count} DB rows matched JSON"


if __name__ == "__main__":
    rc = pytest.main(["-x", __file__] + sys.argv[1:])
    sys.exit(rc)
