from types import SimpleNamespace

import pytest
import torch
import torch.distributed as dist

from vllm_omni.diffusion.distributed.autoencoders.distributed_vae_executor import (
    DistributedVaeExecutor,
    DistributedVaeMixin,
    GridSpec,
    TileTask,
)

pytestmark = [pytest.mark.core_model, pytest.mark.cpu]


class E2EOperator:
    """tiles with (2, 3) -- (H,W)"""

    def split(self, z):
        rows_num = 2
        cols_num = 3

        h_size = z.shape[0]
        w_size = z.shape[1]

        tasks = []
        for i in range(rows_num):
            for j in range(cols_num):
                tasks.append(
                    TileTask(
                        tile_id=len(tasks),
                        grid_coord=(i, j),
                        tensor=z[
                            ((i * h_size) // rows_num) : (((i + 1) * h_size) // rows_num),
                            ((j * w_size) // cols_num) : (((j + 1) * w_size) // cols_num),
                        ],
                    )
                )

        grid_spec = GridSpec(
            split_dims=(0, 1),
            grid_shape=(2, 3),
        )
        return tasks, grid_spec

    def exec(self, task: TileTask):
        return torch.full_like(task.tensor, fill_value=task.tile_id)

    def merge(self, coord_tensor_map, grid_spec):
        tiles = []
        for r in range(grid_spec.grid_shape[0]):
            row_tiles = []
            for c in range(grid_spec.grid_shape[1]):
                coord = (r, c)
                row_tiles.append(coord_tensor_map[coord])
            tiles.append(torch.cat(row_tiles, dim=1))
        return torch.cat(tiles, dim=0)


class DummyMixin(DistributedVaeMixin):
    def __init__(self):
        self.use_tiling = True
        self.distributed_executor = SimpleNamespace(parallel_size=2, group=None)


@pytest.fixture(autouse=True)
def mock_dist(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setattr(dist, "get_world_size", lambda *args, **kwargs: 2)
    monkeypatch.setattr(dist, "get_rank", lambda *args, **kwargs: 0)
    monkeypatch.setattr(dist, "is_initialized", lambda: True)
    monkeypatch.setattr(dist, "all_reduce", lambda *args, **kwargs: None)
    monkeypatch.setattr(dist, "gather", lambda *args, **kwargs: None)
    monkeypatch.setattr(dist, "broadcast", lambda *args, **kwargs: None)


@pytest.fixture(autouse=True)
def mock_dit_group(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setattr(
        "vllm_omni.diffusion.distributed.autoencoders.distributed_vae_executor.get_dit_group",
        lambda: None,
    )


@pytest.fixture(autouse=True)
def mock_dist_vae_executor(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setattr(DistributedVaeExecutor, "gather_tensors", lambda self, x: [x])
    monkeypatch.setattr(DistributedVaeExecutor, "broadcast_tensor", lambda self, x: x)


# ============================
# Unitest
# ============================


def test_balance_tasks():
    executor = DistributedVaeExecutor()
    workloads = [2, 5, 13, 8, 2, 4]  # each is 17
    tasks = [TileTask(0, (i,), torch.tensor([i]), workload=load) for i, load in enumerate(workloads)]
    assigned = executor._balance_tasks(tasks, num_rank=2)
    assert len(assigned) == 2
    total_work = [sum(t.workload for t in group) for group in assigned]
    assert total_work[0] == total_work[1]


def test_compute_global_padding_shape():
    executor = DistributedVaeExecutor()
    executor.rank = 0

    local_results = [(0, torch.zeros((2, 3))), (1, torch.zeros((4, 2)))]
    shape = executor._compute_global_padding_shape(local_results, 2, "cpu")

    assert shape == [2, 4, 3]


def test_pack_and_unpack():
    executor = DistributedVaeExecutor()
    executor.world_size = 1

    grid_spec = GridSpec(split_dims=(0, 1), grid_shape=(2, 2))

    # ======================
    # pack
    # ======================
    local_results = [(0, torch.tensor([[1, 2], [3, 4]]))]

    global_shape = [3, 3, 3]  # (tiles, H, W)

    tile_tensor, meta_tensor = executor._pack_local_tiles(
        local_results, global_shape, grid_spec, device="cpu", dtype=torch.int64
    )

    # check pack
    assert tile_tensor.shape == torch.Size(global_shape)
    assert meta_tensor.shape == (global_shape[0], len(grid_spec.split_dims) + 1)
    assert meta_tensor[0, 0] == 0
    assert meta_tensor[0, 1] == 2
    assert meta_tensor[0, 2] == 2

    # ======================
    # unpack
    # ======================
    meta_gather = [meta_tensor]
    tile_gather = [tile_tensor]

    tid_coord_map = {0: (0, 0)}

    coord_tensor_map = executor._unpack_tiles(meta_gather, tile_gather, grid_spec, tid_coord_map)

    # check unpack
    assert torch.equal(coord_tensor_map[(0, 0)], torch.tensor([[1, 2], [3, 4]]))


def test_is_distributed_enabled():
    mixin = DummyMixin()
    assert mixin.is_distributed_enabled() is True

    mixin.use_tiling = False
    assert mixin.is_distributed_enabled() is False
