#!/usr/bin/env python3
"""Tests for the updated extract_match function."""

import sys
import os
from unittest.mock import patch

# Add the project root to the Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

from mbetterclient.core.games_thread import extract_match, extract_balanced_safe, extract_balanced_candidate_v2, extract_balanced_candidate_v3, extract_balanced_candidate_v4


def _simulate_payout_error(controller, results, odds, payouts, stake, house_edge, rounds, seed):
    import random

    rng = random.Random(seed)
    original_choices = random.choices
    current_balance = 0.0
    expected = stake * (1 - house_edge)
    total_error = 0.0

    def deterministic_choices(population, weights, k=1):
        total = sum(weights)
        pick = rng.random() * total
        cumulative = 0.0
        for item, weight in zip(population, weights):
            cumulative += weight
            if pick <= cumulative:
                return [item]
        return [population[-1]]

    random.choices = deterministic_choices
    try:
        for _ in range(rounds):
            selected, impact = controller(results, odds, current_balance, stake, payouts, house_edge=house_edge)
            current_balance += impact
            total_error += abs(payouts[selected] - expected)
    finally:
        random.choices = original_choices

    return total_error


def _run_three_way_benchmark(results, odds, payouts, stake, house_edge, rounds, seed):
    return {
        "current": _simulate_payout_error(
            extract_balanced_safe,
            results,
            odds,
            payouts,
            stake=stake,
            house_edge=house_edge,
            rounds=rounds,
            seed=seed,
        ),
        "candidate_v2": _simulate_payout_error(
            extract_balanced_candidate_v2,
            results,
            odds,
            payouts,
            stake=stake,
            house_edge=house_edge,
            rounds=rounds,
            seed=seed,
        ),
        "candidate_v3": _simulate_payout_error(
            extract_balanced_candidate_v3,
            results,
            odds,
            payouts,
            stake=stake,
            house_edge=house_edge,
            rounds=rounds,
            seed=seed,
        ),
        "candidate_v4": _simulate_payout_error(
            extract_balanced_candidate_v4,
            results,
            odds,
            payouts,
            stake=stake,
            house_edge=house_edge,
            rounds=rounds,
            seed=seed,
        ),
        "legacy": _simulate_payout_error(
            _legacy_extract_balanced_safe,
            results,
            odds,
            payouts,
            stake=stake,
            house_edge=house_edge,
            rounds=rounds,
            seed=seed,
        ),
    }


def _build_benchmark_scenarios(count=100):
    import random

    rng = random.Random(101)
    scenarios = []
    for index in range(count):
        results = [f"R{index}_LOW", f"R{index}_MID", f"R{index}_HIGH"]
        low_odd = round(rng.uniform(1.7, 2.4), 2)
        mid_odd = round(rng.uniform(2.3, 3.4), 2)
        high_odd = round(rng.uniform(4.0, 6.8), 2)
        low_payout = round(rng.uniform(40.0, 58.0), 2)
        mid_payout = round(rng.uniform(62.0, 82.0), 2)
        high_payout = round(rng.uniform(90.0, 130.0), 2)
        scenarios.append(
            {
                "results": results,
                "odds": [low_odd, mid_odd, high_odd],
                "payouts": {
                    results[0]: low_payout,
                    results[1]: mid_payout,
                    results[2]: high_payout,
                },
                "seed": 1000 + index,
            }
        )
    return scenarios


def _legacy_extract_balanced_safe(results, odds, current_balance, stake, payouts, house_edge=0.10, sensitivity=0.01, max_drift=0.4):
    import random

    raw_probs = [1.0 / o for o in odds]
    total_raw = sum(raw_probs)
    target_total = 1.0 + house_edge
    edged_probs = [(p / total_raw) * target_total for p in raw_probs]

    final_weights = []
    expected_redistribution = stake * (1 - house_edge)
    for i, p in enumerate(edged_probs):
        payout = payouts.get(results[i], 0)
        net_change = expected_redistribution - payout
        if current_balance < 0:
            correction = 1.0 + (abs(current_balance) * net_change * sensitivity / 100)
        else:
            correction = 1.0 + (-abs(current_balance) * net_change * sensitivity / 100)
        lower_bound = p * (1 - max_drift)
        upper_bound = p * (1 + max_drift)
        corrected = p * correction
        final_weights.append(max(min(corrected, upper_bound), lower_bound))

    selection = random.choices(results, weights=final_weights, k=1)[0]
    return selection, expected_redistribution - payouts[selection]


def test_extract_match_updated_executes_successfully():
    """Test the updated extract_match function with separate uopayin and mpayin parameters."""
    balance = -50.0
    uopayin = 100.0
    mpayin = 200.0
    match = 1
    cap = 70.0
    payouts = {
        "UNDER": 80.0,
        "OVER": 90.0,
        "RESULTS": {
            "KO1": ["WIN1", 150.0],
            "WIN1": ["WIN1", 180.0],
        },
    }
    odds = {
        "UNDER": 2.0,
        "OVER": 1.8,
        "KO1": 3.0,
        "WIN1": 2.5,
    }

    uores, mres, impact, winning_out = extract_match(balance, uopayin, mpayin, match, cap, payouts, odds)

    assert uores in {"UNDER", "OVER"}
    assert isinstance(mres, str)
    assert isinstance(impact, float)
    assert isinstance(winning_out, str)


def test_extract_match_handles_zero_values():
    """Test that the function safely handles matches with no result outcomes."""
    balance = 0.0
    uopayin = 0.0
    mpayin = 0.0
    match = 1
    cap = 50.0
    payouts = {
        "UNDER": 0.0,
        "OVER": 0.0,
        "RESULTS": {},
    }
    odds = {
        "UNDER": 1.0,
        "OVER": 1.0,
    }

    uores, mres, impact, winning_out = extract_match(balance, uopayin, mpayin, match, cap, payouts, odds)

    assert uores in {"UNDER", "OVER"}
    assert mres == "N/A"
    assert impact == 0.0
    assert winning_out == ""


def test_extract_match_returns_all_associated_winning_outcomes():
    """Test that extract_match preserves all associated winning outcomes for the selected result."""
    balance = 0.0
    uopayin = 50.0
    mpayin = 100.0
    match = 2
    cap = 70.0
    payouts = {
        "UNDER": 40.0,
        "OVER": 45.0,
        "RESULTS": {
            "WIN1": ["WIN1, KO1", 120.0],
            "KO1": ["KO1", 90.0],
        },
    }
    odds = {
        "UNDER": 2.0,
        "OVER": 1.9,
        "WIN1": 2.5,
        "KO1": 3.0,
    }

    with patch("mbetterclient.core.games_thread.random.choices", side_effect=[["UNDER"], ["WIN1"]]):
        uores, mres, impact, winning_out = extract_match(balance, uopayin, mpayin, match, cap, payouts, odds)

    assert uores == "UNDER"
    assert mres == "WIN1"
    assert winning_out == "WIN1, KO1"
    assert abs(impact - ((uopayin * 0.70 - 40.0) + (mpayin * 0.70 - 120.0))) < 1e-9


def test_extract_balanced_safe_long_run_average_stays_near_target():
    """Long-run payout average should stay reasonably close to configured redistribution target."""
    results = ["LOW", "MID", "HIGH"]
    odds = [2.0, 3.0, 5.0]
    payouts = {
        "LOW": 50.0,
        "MID": 70.0,
        "HIGH": 95.0,
    }
    stake = 100.0
    cap = 70.0
    house_edge = 1.0 - (cap / 100.0)
    current_balance = 0.0
    redistributed_total = 0.0
    rounds = 4000

    for _ in range(rounds):
        selected, impact = extract_balanced_safe(
            results,
            odds,
            current_balance,
            stake,
            payouts,
            house_edge=house_edge,
        )
        redistributed_total += payouts[selected]
        current_balance += impact

    average_ratio = redistributed_total / (stake * rounds)
    assert 0.64 <= average_ratio <= 0.76


def test_extract_balanced_safe_tracks_balance_direction():
    """Document current balance-direction behavior under repeated draws."""
    results = ["LOW", "MID", "HIGH"]
    odds = [2.0, 3.0, 5.0]
    payouts = {
        "LOW": 50.0,
        "MID": 70.0,
        "HIGH": 95.0,
    }
    stake = 100.0
    house_edge = 0.30

    positive_balance = 300.0
    negative_balance = -300.0

    positive_impacts = []
    negative_impacts = []
    for _ in range(500):
        _, impact = extract_balanced_safe(results, odds, positive_balance, stake, payouts, house_edge=house_edge)
        positive_impacts.append(impact)
        _, impact = extract_balanced_safe(results, odds, negative_balance, stake, payouts, house_edge=house_edge)
        negative_impacts.append(impact)

    assert sum(positive_impacts) / len(positive_impacts) >= 0
    assert sum(negative_impacts) / len(negative_impacts) >= 0


def test_extract_balanced_safe_biases_toward_target_payout_band():
    """Selection should prefer payouts closer to the adjusted target band."""
    results = ["LOW", "MID", "HIGH"]
    odds = [2.0, 2.0, 2.0]
    payouts = {
        "LOW": 40.0,
        "MID": 70.0,
        "HIGH": 110.0,
    }

    counts = {name: 0 for name in results}
    for _ in range(2000):
        selected, _ = extract_balanced_safe(
            results,
            odds,
            current_balance=0.0,
            stake=100.0,
            payouts=payouts,
            house_edge=0.30,
        )
        counts[selected] += 1

    assert counts["MID"] > counts["LOW"]
    assert counts["MID"] > counts["HIGH"]


def test_extract_balanced_safe_benchmark_documents_target_error_vs_legacy():
    """Document current benchmark error against the legacy heuristic on a fixed stream."""
    results = ["LOW", "MID", "HIGH"]
    odds = [2.0, 3.0, 5.0]
    payouts = {
        "LOW": 50.0,
        "MID": 70.0,
        "HIGH": 95.0,
    }

    metrics = _run_three_way_benchmark(results, odds, payouts, stake=100.0, house_edge=0.30, rounds=1500, seed=11)

    assert metrics["current"] > 0
    assert metrics["candidate_v4"] > 0
    assert metrics["legacy"] > 0
    assert abs(metrics["current"] - metrics["candidate_v4"]) < 1e-9


def test_extract_balanced_candidate_v2_benchmark_documents_three_way_comparison():
    """Compare legacy, current, and candidate_v2 on the same fixed seeded stream."""
    results = ["LOW", "MID", "HIGH"]
    odds = [2.0, 3.0, 5.0]
    payouts = {
        "LOW": 50.0,
        "MID": 70.0,
        "HIGH": 95.0,
    }

    metrics = _run_three_way_benchmark(results, odds, payouts, stake=100.0, house_edge=0.30, rounds=1500, seed=11)

    assert metrics["current"] > 0
    assert metrics["candidate_v2"] > 0
    assert metrics["candidate_v3"] > 0
    assert metrics["legacy"] > 0
    assert abs(metrics["current"] - metrics["candidate_v4"]) < 1e-9


def test_extract_balanced_controller_ordering_across_seeded_scenarios():
    """Compare controller variants across a broad fixed seeded scenario set."""
    scenarios = _build_benchmark_scenarios(100)

    totals = {"current": 0.0, "candidate_v2": 0.0, "candidate_v3": 0.0, "candidate_v4": 0.0, "legacy": 0.0}
    wins = {"current": 0, "candidate_v2": 0, "candidate_v3": 0, "candidate_v4": 0, "legacy": 0}
    per_scenario = []

    for scenario in scenarios:
        metrics = _run_three_way_benchmark(
            scenario["results"],
            scenario["odds"],
            scenario["payouts"],
            stake=100.0,
            house_edge=0.30,
            rounds=1000,
            seed=scenario["seed"],
        )
        for key, value in metrics.items():
            totals[key] += value
        best = min(metrics, key=metrics.get)
        wins[best] += 1
        per_scenario.append({
            "seed": scenario["seed"],
            "metrics": metrics,
            "best": best,
        })

    assert totals["current"] > 0
    assert totals["candidate_v2"] > 0
    assert totals["candidate_v3"] > 0
    assert totals["candidate_v4"] > 0
    assert totals["legacy"] > 0
    assert abs(totals["current"] - totals["candidate_v4"]) < 1e-9
    assert totals["current"] < totals["legacy"]
    assert sum(wins.values()) == len(scenarios)
    assert len(per_scenario) == len(scenarios)

    metrics_summary = " | ".join(
        f"seed={item['seed']} best={item['best']} current={item['metrics']['current']:.1f} "
        f"candidate_v2={item['metrics']['candidate_v2']:.1f} candidate_v3={item['metrics']['candidate_v3']:.1f} "
        f"candidate_v4={item['metrics']['candidate_v4']:.1f} "
        f"legacy={item['metrics']['legacy']:.1f}"
        for item in per_scenario[:10]
    )
    print(f"controller benchmark summary (first 10 of {len(per_scenario)}): {metrics_summary}")

    totals_summary = ", ".join(f"{name}={value:.1f}" for name, value in totals.items())
    wins_summary = ", ".join(f"{name}={count}" for name, count in wins.items())
    print(f"controller benchmark totals: {totals_summary}")
    print(f"controller benchmark wins: {wins_summary}")
