"""Unit tests for the unified SP shard helpers (pure logic, no distributed)."""

import sys

import pytest
import torch

from sglang.multimodal_gen.runtime.distributed import sp_shard_utils as sps
from sglang.multimodal_gen.runtime.distributed.sp_shard_utils import (
    SpShard,
    shard_like,
    tail_attn_meta,
)


def _fake_sp(monkeypatch, sp_size, sp_rank=0, ring=1):
    monkeypatch.setattr(sps, "get_sp_world_size", lambda: sp_size)
    monkeypatch.setattr(sps, "get_sp_parallel_rank", lambda: sp_rank)
    monkeypatch.setattr(sps, "get_ring_parallel_world_size", lambda: ring)


# --- build_shard_plan math --------------------------------------------------------


def test_plan_shard_divisible(monkeypatch):
    _fake_sp(monkeypatch, 2, 1)
    s = sps.build_shard_plan(16)
    assert (s.local_len, s.num_pad, s.local_pad) == (8, 0, 0)


def test_plan_shard_padded_last_rank(monkeypatch):
    _fake_sp(monkeypatch, 4, 3)
    s = sps.build_shard_plan(14)
    assert (s.local_len, s.num_pad) == (4, 2)
    assert s.local_pad == 2 and s.local_real_len == 2


def test_plan_shard_pad_only_on_last_rank(monkeypatch):
    _fake_sp(monkeypatch, 4, 0)
    s = sps.build_shard_plan(14)
    assert s.local_pad == 0 and s.local_real_len == 4


def test_plan_shard_sp1_noop(monkeypatch):
    _fake_sp(monkeypatch, 1)
    s = sps.build_shard_plan(15)
    assert (s.local_len, s.num_pad, s.sp_size) == (15, 0, 1)


# --- shard_like -------------------------------------------------------------


def test_shard_like_zero_pads_tail():
    shard = SpShard(orig_len=15, local_len=8, num_pad=1, sp_size=2, sp_rank=1)
    x = torch.arange(15, dtype=torch.float32).unsqueeze(0).unsqueeze(-1)
    local = shard_like(x, shard, dim=1)
    assert local.shape[1] == 8
    assert local[0, -1, 0].item() == 0.0  # tail pad
    assert local[0, 0, 0].item() == 8.0  # rank1 starts at token 8


def test_shard_like_repeat_last():
    shard = SpShard(orig_len=15, local_len=8, num_pad=1, sp_size=2, sp_rank=1)
    x = torch.arange(15, dtype=torch.float32).unsqueeze(-1)
    local = shard_like(x, shard, dim=0, pad_mode="repeat_last")
    assert local[-1, 0].item() == 14.0  # repeated last row, not zero


def test_shard_like_chunks_align_across_tensors():
    # RoPE cache sharded with the same plan stays aligned with hidden states.
    shard = SpShard(orig_len=15, local_len=8, num_pad=1, sp_size=2, sp_rank=0)
    x = torch.arange(15).unsqueeze(0).unsqueeze(-1).float()
    rope = torch.arange(15).unsqueeze(-1).float()
    assert torch.equal(
        shard_like(x, shard, dim=1)[0, :, 0], shard_like(rope, shard, dim=0)[:, 0]
    )


# --- tail_attn_meta ---------------------------------------------------------


def test_tail_meta_none_when_divisible():
    shard = SpShard(orig_len=16, local_len=8, num_pad=0, sp_size=2, sp_rank=0)
    assert tail_attn_meta(shard, 1, torch.device("cpu")) is None


def test_tail_meta_single_stream():
    shard = SpShard(orig_len=15, local_len=8, num_pad=1, sp_size=2, sp_rank=1)
    meta = tail_attn_meta(shard, 1, torch.device("cpu"))
    assert meta["pad_start"] == 15 and meta["pad_end"] == 16
    assert meta["local_pad"] == 1
    assert meta["cu_seqlens_tail"].tolist() == [0, 15, 16]
    assert meta["max_seqlen_tail"] == 15


def test_tail_meta_joint_layout_and_batch():
    # sp=2, local_txt=8 (1 pad), img=100 per rank -> S = 2*(8+100) = 216.
    shard = SpShard(orig_len=15, local_len=8, num_pad=1, sp_size=2, sp_rank=1)
    meta = tail_attn_meta(shard, 2, torch.device("cpu"), image_seq_len=100)
    assert meta["pad_start"] == 215 and meta["pad_end"] == 216
    assert meta["cu_seqlens_tail"].tolist() == [0, 215, 216, 431, 432]


def test_tail_meta_max_seqlen_covers_pad_segment():
    # Degenerate short sequence: num_pad (3) > valid (1). FA requires
    # max_seqlen >= the longest segment, i.e. the pad block here.
    shard = SpShard(orig_len=1, local_len=1, num_pad=3, sp_size=4, sp_rank=3)
    meta = tail_attn_meta(shard, 1, torch.device("cpu"))
    assert meta["max_seqlen_tail"] == 3


def test_tail_meta_matches_legacy_gap_formula():
    # The tail layout puts the pad exactly where the legacy per-model gap
    # formula pointed, minus the relocation: end == S (global tail).
    sp, local_txt, img, num_pad = 3, 5, 40, 2
    shard = SpShard(
        orig_len=sp * local_txt - num_pad,
        local_len=local_txt,
        num_pad=num_pad,
        sp_size=sp,
        sp_rank=sp - 1,
    )
    meta = tail_attn_meta(shard, 1, torch.device("cpu"), image_seq_len=img)
    seq = sp * (local_txt + img)
    assert meta["pad_end"] == seq
    assert meta["pad_start"] == seq - num_pad


# --- plan_text_strategy -----------------------------------------------------


def test_strategy_sp1_replicates(monkeypatch):
    _fake_sp(monkeypatch, 1)
    assert sps.plan_text_strategy(100) == "replicate"


def test_strategy_shard_when_legal(monkeypatch):
    _fake_sp(monkeypatch, 2)
    assert sps.plan_text_strategy(15) == "shard"
    assert sps.plan_text_strategy(16) == "shard"


def test_strategy_ring_blocks_padded_shard(monkeypatch):
    _fake_sp(monkeypatch, 2, ring=2)
    assert sps.plan_text_strategy(15) == "replicate"  # padded shard needs mask
    assert sps.plan_text_strategy(16) == "shard"  # divisible: no mask needed


def test_strategy_min_len_threshold(monkeypatch):
    _fake_sp(monkeypatch, 2)
    monkeypatch.setattr(sps, "_TEXT_SHARD_MIN", 64)
    assert sps.plan_text_strategy(32) == "replicate"
    assert sps.plan_text_strategy(64) == "shard"


# --- join_seqs / split_seqs / shard_seq_prefix ------------------------------


def test_join_split_roundtrip_with_pad():
    # Joint [text, image] with 2 tail-pad rows relocated behind the image.
    txt = torch.arange(6, dtype=torch.float32).view(1, 6, 1)  # rows 4,5 are pad
    img = (torch.arange(3, dtype=torch.float32) + 100).view(1, 3, 1)
    joint = sps.join_seqs(txt, img, local_pad=2)
    assert joint[0, :, 0].tolist() == [0, 1, 2, 3, 100, 101, 102, 4, 5]
    txt_back, img_back = sps.split_seqs(joint, prefix_len=6, local_pad=2)
    assert torch.equal(txt_back, txt) and torch.equal(img_back, img)


def test_join_split_roundtrip_no_pad():
    txt = torch.randn(1, 4, 2)
    img = torch.randn(1, 3, 2)
    joint = sps.join_seqs(txt, img, local_pad=0)
    assert torch.equal(joint, torch.cat([txt, img], dim=1))
    txt_back, img_back = sps.split_seqs(joint, prefix_len=4, local_pad=0)
    assert torch.equal(txt_back, txt) and torch.equal(img_back, img)


def test_shard_seq_prefix_only_touches_prefix():
    # Joint RoPE cache [txt(15); img(4)]: text segment shards, image stays.
    shard = SpShard(orig_len=15, local_len=8, num_pad=1, sp_size=2, sp_rank=1)
    cache = torch.arange(19, dtype=torch.float32).unsqueeze(-1)
    out = sps.shard_seq_prefix(cache, 15, shard, dim=0)
    assert out.shape[0] == 8 + 4
    assert out[0, 0].item() == 8.0  # rank1 text chunk starts at token 8
    assert out[-4:, 0].flatten().tolist() == [15, 16, 17, 18]  # image untouched


def test_should_shard_text_gate(monkeypatch):
    _fake_sp(monkeypatch, 2)
    assert sps.should_shard_text(15) is True
    _fake_sp(monkeypatch, 1)
    assert sps.should_shard_text(15) is False


# --- gather_seq -------------------------------------------------------------


def test_gather_seq_sp1_noop(monkeypatch):
    _fake_sp(monkeypatch, 1)
    x = torch.randn(1, 5, 2)
    assert sps.gather_seq(x, 5, dim=1) is x


def test_gather_seq_trims(monkeypatch):
    _fake_sp(monkeypatch, 2)
    monkeypatch.setattr(
        sps, "sequence_model_parallel_all_gather", lambda t, dim: torch.cat([t, t], dim)
    )
    local = torch.randn(1, 8, 2)
    out = sps.gather_seq(local, 15, dim=1)
    assert out.shape[1] == 15


if __name__ == "__main__":
    sys.exit(pytest.main([__file__, "-q"]))
