# Copyright Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT

"""
Ranking Regression Tests for Origami

These tests verify that config rankings remain stable across code changes.
Rankings are compared against golden baseline files to detect unintended
changes from PRs.

Usage:
    # Run tests (compares against baseline)
    pytest test_ranking_regression.py -v

    # Generate new baseline files (run from develop branch)
    pytest test_ranking_regression.py -v --generate-baseline

    # Update baseline for specific architecture
    pytest test_ranking_regression.py -v --generate-baseline -k gfx942
"""

import csv
from functools import lru_cache
from pathlib import Path

import pytest
import yaml

import origami
from helpers import HARDWARE, create_config_list, get_matrix_instructions


BASELINE_DIR = Path(__file__).parent / "baselines" / "rankings"
PROBLEM_DATA_FILE = Path(__file__).parent / "data" / "problem_data.csv"

SUPPORTED_DTYPES = ["f16", "bf16", "f32", "xf32", "f8"]
TRANSPOSE_VALUES = [origami.transpose_t.T, origami.transpose_t.N]

TOP_K = 5


def is_dtype_supported(arch_name: str, dtype: str) -> bool:
    """Check if a dtype is supported for the given architecture."""
    hardware = HARDWARE[arch_name]
    return len(get_matrix_instructions(hardware, dtype)) > 0

def load_problem_sizes() -> list[tuple[int, int, int, int]]:
    """Load problem sizes from CSV file.
    
    Returns:
        List of (m, n, k, batch) tuples.
    """
    problems = []
    with open(PROBLEM_DATA_FILE, "r") as f:
        reader = csv.DictReader(f)
        for row in reader:
            m = int(row["m"])
            n = int(row["n"])
            k = int(row["k"])
            batch = int(row["batch_count"])
            problems.append((m, n, k, batch))
    return problems


TEST_PROBLEM_SIZES = load_problem_sizes()


def create_problem(
    m: int,
    n: int,
    k: int,
    dtype: str,
    batch: int = 1,
    transA: origami.transpose_t = origami.transpose_t.T,
    transB: origami.transpose_t = origami.transpose_t.N,
) -> origami.problem_t:
    """Create a problem specification."""
    problem = origami.problem_t()
    problem.size = origami.dim3_t(m, n, k)
    problem.batch = batch
    problem.a_transpose = transA
    problem.b_transpose = transB
    problem.a_dtype = origami.string_to_datatype(dtype)
    problem.b_dtype = origami.string_to_datatype(dtype)
    problem.d_dtype = origami.string_to_datatype(dtype)
    problem.c_dtype = problem.d_dtype
    problem.mi_dtype = problem.a_dtype
    problem.a_mx_block_size = 0
    problem.b_mx_block_size = 0
    return problem


def config_to_tuple(config: origami.config_t) -> tuple:
    """Convert config_t to a compact tuple."""
    return (
        config.mt.m, config.mt.n, config.mt.k,
        config.mi.m, config.mi.n, config.mi.k,
        config.occupancy, config.workgroup_mapping,
    )


def result_to_config_tuple(result: origami.prediction_result_t) -> list[int]:
    """Convert prediction result to a config tuple [mt_m, mt_n, mt_k, mi_m, mi_n, mi_k, occ, wgm]."""
    cfg = result.config
    return [
        cfg.mt.m, cfg.mt.n, cfg.mt.k,
        cfg.mi.m, cfg.mi.n, cfg.mi.k,
        cfg.occupancy, cfg.workgroup_mapping,
    ]


def generate_rankings(
    arch_name: str,
    dtype: str,
    transA: origami.transpose_t = origami.transpose_t.T,
    transB: origami.transpose_t = origami.transpose_t.N,
) -> dict[str, list[list[int]]]:
    """Generate rankings for all test problem sizes.
    
    Returns a dict mapping problem_key -> list of config tuples.
    Each config tuple is [mt_m, mt_n, mt_k, mi_m, mi_n, mi_k, occ, wgm].
    """
    hardware = HARDWARE[arch_name]
    configs = create_config_list(
        hardware, dtype,
        mt_sizes=[16, 32, 48, 96, 128, 192, 224, 256, 336, 448, 512],
        depth_unroll=[16, 32, 64, 128, 512, 1024],
        occupancy_values=[1, 2],
        wgm_values=[1, 4, 8],
    )

    if not configs:
        return {}

    rankings = {}
    for m, n, k, batch in TEST_PROBLEM_SIZES:
        problem = create_problem(m, n, k, dtype, batch, transA, transB)
        try:
            ranked = origami.select_topk_configs(problem, hardware, configs, TOP_K, origami.model_t.gemm)
            if ranked:
                key = f"{m}x{n}x{k}x{batch}"
                rankings[key] = [result_to_config_tuple(r) for r in ranked]
        except Exception:
            pass

    return rankings


try:
    from yaml import CSafeDumper as SafeDumper, CSafeLoader as SafeLoader
except ImportError:
    from yaml import SafeDumper, SafeLoader


def get_baseline_path(arch_name: str) -> Path:
    """Get the path to the baseline file for an architecture."""
    return BASELINE_DIR / f"{arch_name}.yaml"


@lru_cache(maxsize=None)
def load_arch_baseline(arch_name: str) -> dict | None:
    """Load and cache the full baseline for an architecture.
    
    Returns the parsed YAML dict, or None if file doesn't exist.
    """
    path = get_baseline_path(arch_name)
    if not path.exists():
        return None
    
    with open(path, "r") as f:
        return yaml.load(f, Loader=SafeLoader)


def transpose_key(transA: origami.transpose_t, transB: origami.transpose_t) -> str:
    """Convert transpose values to a string key for baseline storage."""
    a = "T" if transA == origami.transpose_t.T else "N"
    b = "T" if transB == origami.transpose_t.T else "N"
    return f"{a}{b}"


def load_baseline(
    arch_name: str, dtype: str, transA: origami.transpose_t, transB: origami.transpose_t
) -> dict[str, list[list[int]]] | None:
    """Load baseline rankings for a specific arch/dtype/transpose combination."""
    baseline = load_arch_baseline(arch_name)
    if baseline is None:
        return None
    
    try:
        return baseline[dtype][transpose_key(transA, transB)]
    except KeyError:
        return None


def save_baseline(
    arch_name: str,
    dtype: str,
    transA: origami.transpose_t,
    transB: origami.transpose_t,
    rankings: dict[str, list[list[int]]],
) -> None:
    """Save rankings to the architecture's YAML baseline file."""
    BASELINE_DIR.mkdir(parents=True, exist_ok=True)
    path = get_baseline_path(arch_name)
    
    if path.exists():
        with open(path, "r") as f:
            baseline = yaml.load(f, Loader=SafeLoader) or {}
    else:
        baseline = {}
    
    if dtype not in baseline:
        baseline[dtype] = {}
    baseline[dtype][transpose_key(transA, transB)] = rankings
    
    # Write with compact format: flow style for problem entries, sorted keys
    # Custom representer to use flow style for the innermost lists (config lists per problem)
    class CompactDumper(SafeDumper):
        pass
    
    def represent_problem_configs(dumper, data):
        # Use flow style for list of config lists (the value under each problem key)
        return dumper.represent_sequence('tag:yaml.org,2002:seq', data, flow_style=True)
    
    # Apply flow style to lists that contain lists (problem -> [[config], [config], ...])
    CompactDumper.add_representer(list, represent_problem_configs)
    
    with open(path, "w") as f:
        yaml.dump(baseline, f, Dumper=CompactDumper, default_flow_style=False, sort_keys=True, width=1000)
    
    # Clear cache so subsequent loads see the updated file
    load_arch_baseline.cache_clear()


def compare_rankings(
    current: dict[str, list[list[int]]], baseline: dict[str, list[list[int]]]
) -> list[str]:
    """
    Compare current rankings against baseline.

    Returns a list of differences found.
    """
    differences = []

    for problem_key, base_configs in baseline.items():
        if problem_key not in current:
            differences.append(f"Missing problem: {problem_key}")
            continue

        curr_configs = current[problem_key]
        
        if len(curr_configs) != len(base_configs):
            differences.append(
                f"{problem_key}: Different rank count (curr={len(curr_configs)}, base={len(base_configs)})"
            )

        for rank_idx, (curr_cfg, base_cfg) in enumerate(zip(curr_configs, base_configs)):
            if curr_cfg != base_cfg:
                differences.append(
                    f"{problem_key} rank {rank_idx}: Config mismatch\n"
                    f"  Current:  MT={tuple(curr_cfg[0:3])}, MI={tuple(curr_cfg[3:6])}, occ={curr_cfg[6]}, wgm={curr_cfg[7]}\n"
                    f"  Baseline: MT={tuple(base_cfg[0:3])}, MI={tuple(base_cfg[3:6])}, occ={base_cfg[6]}, wgm={base_cfg[7]}"
                )

    for problem_key in current:
        if problem_key not in baseline:
            differences.append(f"New problem not in baseline: {problem_key}")

    return differences


@pytest.mark.regression
class TestRankingRegression:
    """Test suite for ranking regression tests."""

    @pytest.mark.parametrize("arch_name", list(HARDWARE.keys()))
    @pytest.mark.parametrize("dtype", SUPPORTED_DTYPES)
    @pytest.mark.parametrize("transA", TRANSPOSE_VALUES)
    @pytest.mark.parametrize("transB", TRANSPOSE_VALUES)
    def test_ranking_stability(
        self,
        arch_name: str,
        dtype: str,
        transA: origami.transpose_t,
        transB: origami.transpose_t,
        generate_baseline: bool,
    ):
        """Test that rankings remain stable compared to baseline."""
        trans_str = transpose_key(transA, transB)

        if not is_dtype_supported(arch_name, dtype):
            pytest.skip(f"No {dtype} support for {arch_name}")

        current = generate_rankings(arch_name, dtype, transA, transB)

        if not current:
            pytest.skip(f"No valid configs generated for {arch_name}/{dtype}/{trans_str}")

        if generate_baseline:
            save_baseline(arch_name, dtype, transA, transB, current)
            pytest.skip(f"Generated baseline for {arch_name}/{dtype}/{trans_str}")

        baseline = load_baseline(arch_name, dtype, transA, transB)
        if baseline is None:
            pytest.fail(
                f"No baseline file found for {arch_name}/{dtype}/{trans_str}. "
                f"Run with --generate-baseline to create it."
            )

        differences = compare_rankings(current, baseline)

        if differences:
            diff_summary = "\n".join(differences[:10])
            if len(differences) > 10:
                diff_summary += f"\n... and {len(differences) - 10} more differences"
            pytest.fail(
                f"Ranking regression detected for {arch_name}/{dtype}/{trans_str}:\n{diff_summary}"
            )
