from __future__ import annotations

import jax
import jax.numpy as jnp

from lost_cities_jax import reset
from lost_cities_jax.league import (
    LeagueConfig,
    PoolMember,
    active_training_pool,
    load_league_config,
    pool_sampling_probabilities,
)
from lost_cities_jax.opponents import policy_by_name
from lost_cities_jax.ppo import (
    JaxPPOConfig,
    NetworkConfig,
    OpponentConfig,
    PPOHyperConfig,
    RunConfig,
    create_train_state,
    make_league_train_iteration,
    sample_league_assignments,
)


def tiny_league_ppo_config(tmp_path) -> JaxPPOConfig:
    return JaxPPOConfig(
        run=RunConfig(
            experiment_name="pytest-jax-ppo-league",
            seed=123,
            total_updates=1,
            checkpoint_every=1,
            artifact_root=str(tmp_path),
        ),
        opponent=OpponentConfig(name="discard_only"),
        network=NetworkConfig(hidden_size=32, num_layers=1),
        ppo=PPOHyperConfig(batch_games=8, rollout_steps=16, epochs=1, minibatches=2),
    )


def test_league_config_loads():
    cfg = load_league_config("configs/jax_ppo/league-v1.yaml")
    assert cfg.experiment_name == "jax-ppo-league-v1"
    assert cfg.cycles == 5
    assert len(cfg.anchors) == 7
    assert any(member.name == "heuristic_expert" for member in cfg.anchors)


def test_pfsp_probabilities_cap_stalling_anchors():
    cfg = LeagueConfig(
        base_config="configs/jax_ppo/ladder-v2-expert.yaml",
        warm_start_checkpoint="unused",
        stalling_anchor_floor=0.02,
        stalling_anchor_cap=0.05,
        uniform_mix=0.1,
    )
    pool = [
        PoolMember("discard_only", "static", stalling=True, recent_win_rate=0.01),
        PoolMember("heuristic_expert", "static", recent_win_rate=0.5),
        PoolMember("snapshot", "checkpoint", anchor=False, recent_win_rate=0.2),
    ]
    probs = pool_sampling_probabilities(pool, cfg)
    assert abs(sum(probs) - 1.0) < 1.0e-6
    assert 0.02 <= probs[0] <= 0.05


def test_active_training_pool_keeps_anchors_and_hard_nonanchors():
    cfg = LeagueConfig(
        base_config="configs/jax_ppo/ladder-v2-expert.yaml",
        warm_start_checkpoint="unused",
        max_active_pool_members=4,
    )
    pool = [
        PoolMember("anchor_a", "static", anchor=True),
        PoolMember("anchor_b", "static", anchor=True),
        PoolMember("easy", "checkpoint", anchor=False, recent_win_rate=0.9),
        PoolMember("hard", "checkpoint", anchor=False, recent_win_rate=0.1),
        PoolMember("medium", "checkpoint", anchor=False, recent_win_rate=0.5),
    ]
    active = active_training_pool(pool, cfg)
    assert [member.name for member in active] == ["anchor_a", "anchor_b", "hard", "medium"]


def test_one_jitted_league_train_iteration_shapes(tmp_path):
    cfg = tiny_league_ppo_config(tmp_path)
    train_state = create_train_state(cfg, jax.random.PRNGKey(0))
    env_state = jax.jit(jax.vmap(reset))(jax.random.split(jax.random.PRNGKey(1), 8))
    probs = jnp.asarray([0.5, 0.5], dtype=jnp.float32)
    assignments = sample_league_assignments(jax.random.PRNGKey(2), 8, probs, 0.5)
    train_iteration = make_league_train_iteration(
        cfg,
        [policy_by_name("discard_only"), policy_by_name("heuristic_expert")],
        probs,
        0.5,
    )

    train_state, env_state, assignments, rng, metrics = train_iteration(
        train_state,
        env_state,
        assignments,
        jax.random.PRNGKey(3),
        jnp.asarray(0.0, dtype=jnp.float32),
    )

    assert env_state.to_move.shape == (8,)
    assert assignments.learner_seat.shape == (8,)
    assert rng.shape == (2,)
    assert "opened_colors_mean" in metrics
