# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit tests for HSDP (Hybrid Sharded Data Parallel) configuration and utilities.

These tests verify HSDP configuration logic without requiring a distributed environment.
"""

import pytest
import torch.nn as nn

from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.diffusion.distributed.hsdp import HSDPInferenceConfig

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


class TestHSDPInferenceConfig:
    """Tests for HSDPInferenceConfig dataclass."""

    def test_default_values(self):
        """Test default configuration values."""
        config = HSDPInferenceConfig()
        assert config.enabled is False
        assert config.hsdp_replicate_size == 1
        assert config.hsdp_shard_size == -1

    def test_custom_values(self):
        """Test custom configuration values."""
        config = HSDPInferenceConfig(
            enabled=True,
            hsdp_replicate_size=2,
            hsdp_shard_size=4,
        )
        assert config.enabled is True
        assert config.hsdp_replicate_size == 2
        assert config.hsdp_shard_size == 4


class TestDiffusionParallelConfigHSDP:
    """Tests for HSDP settings in DiffusionParallelConfig."""

    def test_hsdp_disabled_by_default(self):
        """HSDP should be disabled by default."""
        config = DiffusionParallelConfig()
        assert config.use_hsdp is False
        assert config.hsdp_shard_size == -1
        assert config.hsdp_replicate_size == 1

    def test_hsdp_auto_shard_size(self):
        """Test auto-calculation of hsdp_shard_size when use_hsdp=True."""
        # ulysses_degree=4 -> world_size=4
        # hsdp_shard_size should be auto-calculated as 4 // 1 = 4
        config = DiffusionParallelConfig(
            ulysses_degree=4,
            use_hsdp=True,
        )
        assert config.world_size == 4
        assert config.hsdp_shard_size == 4
        assert config.hsdp_replicate_size == 1

    def test_hsdp_auto_shard_size_fails_standalone(self):
        """Test that auto-calculate fails when other parallelism is all 1."""
        # When all other parallelism is 1, cannot auto-calculate
        # User must specify hsdp_shard_size explicitly
        with pytest.raises(ValueError, match="Cannot auto-calculate hsdp_shard_size"):
            DiffusionParallelConfig(
                use_hsdp=True,
                # All other parallelism defaults to 1
            )

    def test_hsdp_standalone_mode(self):
        """Test standalone HSDP (HSDP without other parallelism)."""
        # Standalone HSDP: all other parallelism=1, explicit shard_size
        config = DiffusionParallelConfig(
            use_hsdp=True,
            hsdp_shard_size=4,  # Explicit shard size
            hsdp_replicate_size=1,
        )
        # world_size should be determined by HSDP
        assert config.world_size == 4
        assert config.hsdp_shard_size == 4
        assert config.hsdp_replicate_size == 1

    def test_hsdp_standalone_with_replicate(self):
        """Test standalone HSDP with replication."""
        config = DiffusionParallelConfig(
            use_hsdp=True,
            hsdp_shard_size=4,
            hsdp_replicate_size=2,
        )
        # world_size = shard_size * replicate_size
        assert config.world_size == 8
        assert config.hsdp_shard_size == 4
        assert config.hsdp_replicate_size == 2

    def test_hsdp_with_replicate(self):
        """Test HSDP with replication (hybrid mode) combined with other parallelism."""
        # world_size=8, replicate=2 -> shard_size should be 4
        config = DiffusionParallelConfig(
            ulysses_degree=8,
            use_hsdp=True,
            hsdp_replicate_size=2,
        )
        assert config.world_size == 8
        assert config.hsdp_shard_size == 4
        assert config.hsdp_replicate_size == 2

    def test_hsdp_explicit_shard_size_valid(self):
        """Test explicit hsdp_shard_size that matches world_size."""
        config = DiffusionParallelConfig(
            ulysses_degree=4,
            use_hsdp=True,
            hsdp_shard_size=4,
            hsdp_replicate_size=1,
        )
        assert config.hsdp_shard_size == 4

    def test_hsdp_explicit_shard_size_invalid(self):
        """Test that invalid HSDP dimensions raise an error when combined with other parallelism."""
        with pytest.raises(ValueError, match="HSDP dimensions"):
            DiffusionParallelConfig(
                ulysses_degree=4,  # world_size=4
                use_hsdp=True,
                hsdp_shard_size=3,  # 1 * 3 != 4
                hsdp_replicate_size=1,
            )

    def test_hsdp_replicate_size_exceeds_world_size(self):
        """Test that replicate_size > world_size raises an error."""
        with pytest.raises(ValueError, match="replicate_size.*must evenly divide world_size"):
            DiffusionParallelConfig(
                ulysses_degree=4,  # world_size=4
                use_hsdp=True,
                hsdp_replicate_size=8,  # 8 > 4, invalid
            )

    def test_hsdp_combined_world_size(self):
        """Test that combined HSDP matches other parallelism world_size."""
        config_no_hsdp = DiffusionParallelConfig(ulysses_degree=4)
        config_with_hsdp = DiffusionParallelConfig(
            ulysses_degree=4,
            use_hsdp=True,
            hsdp_shard_size=4,
        )
        # When combined with other parallelism, world_size should match
        assert config_no_hsdp.world_size == config_with_hsdp.world_size == 4

    def test_hsdp_standalone_world_size(self):
        """Test that standalone HSDP determines world_size."""
        config_hsdp = DiffusionParallelConfig(
            use_hsdp=True,
            hsdp_shard_size=8,
        )
        # Standalone HSDP: world_size is determined by HSDP
        assert config_hsdp.world_size == 8

    def test_hsdp_cannot_use_with_tp(self):
        """Test that HSDP and Tensor Parallelism cannot be used together."""
        with pytest.raises(ValueError, match="cannot be used with TP or DP"):
            DiffusionParallelConfig(
                tensor_parallel_size=2,
                use_hsdp=True,
                hsdp_shard_size=4,
            )

    def test_hsdp_cannot_use_with_dp(self):
        """Test that HSDP and Data Parallelism cannot be used together."""
        with pytest.raises(ValueError, match="cannot be used with TP or DP"):
            DiffusionParallelConfig(
                data_parallel_size=2,
                use_hsdp=True,
                hsdp_shard_size=4,
            )

    def test_from_dict_with_hsdp(self):
        """Test creating config from dict with HSDP settings."""
        config = DiffusionParallelConfig.from_dict(
            {
                "ulysses_degree": 4,
                "use_hsdp": True,
                "hsdp_replicate_size": 2,
            }
        )
        assert config.use_hsdp is True
        assert config.hsdp_replicate_size == 2
        assert config.hsdp_shard_size == 2  # auto: 4 // 2


class TestStandaloneHSDPDetection:
    """Tests for standalone HSDP detection and dit_parallel_size calculation.

    These tests verify the logic used in initialize_model_parallel() to detect
    standalone HSDP mode and calculate effective parallel sizes.

    Standalone HSDP is when all non-HSDP parallelism dimensions are 1.
    """

    @staticmethod
    def compute_standalone_hsdp_params(
        data_parallel_size: int = 1,
        cfg_parallel_size: int = 1,
        sequence_parallel_size: int = 1,
        pipeline_parallel_size: int = 1,
        tensor_parallel_size: int = 1,
        fully_shard_degree: int = 1,
        hsdp_replicate_size: int = 1,
    ) -> dict:
        """Compute standalone HSDP detection parameters.

        This mirrors the logic in initialize_model_parallel().
        """
        dit_parallel_size = (
            data_parallel_size
            * cfg_parallel_size
            * sequence_parallel_size
            * pipeline_parallel_size
            * tensor_parallel_size
        )

        # Check for standalone HSDP: all non-HSDP parallelism dimensions are 1
        is_standalone_hsdp = dit_parallel_size == 1 and fully_shard_degree > 1

        # For standalone HSDP: use (fully_shard_degree * hsdp_replicate_size)
        if is_standalone_hsdp:
            effective_dit_parallel_size = fully_shard_degree * hsdp_replicate_size
        else:
            effective_dit_parallel_size = dit_parallel_size

        effective_dp_size = (fully_shard_degree * hsdp_replicate_size) if is_standalone_hsdp else data_parallel_size

        return {
            "original_dit_parallel_size": dit_parallel_size,
            "is_standalone_hsdp": is_standalone_hsdp,
            "effective_dit_parallel_size": effective_dit_parallel_size,
            "effective_dp_size": effective_dp_size,
        }

    def test_standalone_hsdp_basic(self):
        """Test basic standalone HSDP detection (shard_size=4, replicate=1)."""
        result = self.compute_standalone_hsdp_params(
            fully_shard_degree=4,
            hsdp_replicate_size=1,
        )
        assert result["original_dit_parallel_size"] == 1
        assert result["is_standalone_hsdp"] is True
        assert result["effective_dit_parallel_size"] == 4
        assert result["effective_dp_size"] == 4

    def test_standalone_hsdp_with_replicate(self):
        """Test standalone HSDP with replication (shard_size=4, replicate=2)."""
        result = self.compute_standalone_hsdp_params(
            fully_shard_degree=4,
            hsdp_replicate_size=2,
        )
        assert result["original_dit_parallel_size"] == 1
        assert result["is_standalone_hsdp"] is True
        assert result["effective_dit_parallel_size"] == 8  # 4 * 2
        assert result["effective_dp_size"] == 8

    def test_combined_hsdp_sp_not_standalone(self):
        """Test HSDP combined with SP is NOT detected as standalone.

        This is a regression test for the bug where the condition
        `dit_parallel_size == fully_shard_degree` incorrectly matched
        combined modes like SP=4 + HSDP=4.
        """
        result = self.compute_standalone_hsdp_params(
            sequence_parallel_size=4,
            fully_shard_degree=4,
            hsdp_replicate_size=1,
        )
        assert result["original_dit_parallel_size"] == 4
        assert result["is_standalone_hsdp"] is False
        # Should NOT override dp_size for combined mode
        assert result["effective_dp_size"] == 1  # original data_parallel_size

    def test_combined_hsdp_cfg_not_standalone(self):
        """Test HSDP combined with CFG is NOT detected as standalone."""
        result = self.compute_standalone_hsdp_params(
            cfg_parallel_size=2,
            fully_shard_degree=4,
            hsdp_replicate_size=1,
        )
        assert result["original_dit_parallel_size"] == 2
        assert result["is_standalone_hsdp"] is False
        assert result["effective_dp_size"] == 1

    def test_combined_hsdp_pp_not_standalone(self):
        """Test HSDP combined with PP is NOT detected as standalone."""
        result = self.compute_standalone_hsdp_params(
            pipeline_parallel_size=2,
            fully_shard_degree=4,
            hsdp_replicate_size=1,
        )
        assert result["original_dit_parallel_size"] == 2
        assert result["is_standalone_hsdp"] is False
        assert result["effective_dp_size"] == 1

    def test_no_hsdp_not_standalone(self):
        """Test that no HSDP (fully_shard_degree=1) is NOT standalone."""
        result = self.compute_standalone_hsdp_params(
            fully_shard_degree=1,
        )
        assert result["original_dit_parallel_size"] == 1
        assert result["is_standalone_hsdp"] is False
        assert result["effective_dp_size"] == 1

    def test_combined_multiple_parallelism_not_standalone(self):
        """Test HSDP combined with multiple parallelism is NOT standalone."""
        result = self.compute_standalone_hsdp_params(
            sequence_parallel_size=2,
            cfg_parallel_size=2,
            fully_shard_degree=4,
            hsdp_replicate_size=1,
        )
        assert result["original_dit_parallel_size"] == 4  # 2 * 2
        assert result["is_standalone_hsdp"] is False
        assert result["effective_dp_size"] == 1

    def test_standalone_hsdp_large_shard(self):
        """Test standalone HSDP with large shard size."""
        result = self.compute_standalone_hsdp_params(
            fully_shard_degree=8,
            hsdp_replicate_size=1,
        )
        assert result["is_standalone_hsdp"] is True
        assert result["effective_dit_parallel_size"] == 8
        assert result["effective_dp_size"] == 8

    def test_standalone_hsdp_large_replicate(self):
        """Test standalone HSDP with large replicate size."""
        result = self.compute_standalone_hsdp_params(
            fully_shard_degree=4,
            hsdp_replicate_size=4,
        )
        assert result["is_standalone_hsdp"] is True
        assert result["effective_dit_parallel_size"] == 16  # 4 * 4
        assert result["effective_dp_size"] == 16


class TestHSDPShardConditions:
    """Tests for _hsdp_shard_conditions matching logic."""

    @staticmethod
    def _is_transformer_block(name: str, module: nn.Module) -> bool:
        """Example shard condition matching transformer blocks."""
        return "blocks" in name and name.split(".")[-1].isdigit()

    def test_condition_matches_blocks(self):
        """Test that condition matches transformer block patterns."""
        cond = self._is_transformer_block
        # Should match
        assert cond("blocks.0", nn.Linear(10, 10)) is True
        assert cond("blocks.15", nn.Linear(10, 10)) is True
        assert cond("transformer.blocks.0", nn.Linear(10, 10)) is True
        # Should not match
        assert cond("blocks", nn.Linear(10, 10)) is False
        assert cond("blocks.norm", nn.Linear(10, 10)) is False
        assert cond("embeddings", nn.Linear(10, 10)) is False

    def test_model_with_shard_conditions(self):
        """Test model with _hsdp_shard_conditions attribute."""

        class MockModel(nn.Module):
            @staticmethod
            def _is_block(name: str, module: nn.Module) -> bool:
                return name.startswith("blocks.") and name.split(".")[-1].isdigit()

            _hsdp_shard_conditions = [_is_block]

            def __init__(self):
                super().__init__()
                self.blocks = nn.ModuleList([nn.Linear(10, 10) for _ in range(2)])

        model = MockModel()
        conditions = getattr(model, "_hsdp_shard_conditions", None)
        assert conditions is not None
        assert len(conditions) == 1

        # Verify conditions work on actual model modules
        matched = []
        for name, module in model.named_modules():
            if any(cond(name, module) for cond in conditions):
                matched.append(name)
        assert "blocks.0" in matched
        assert "blocks.1" in matched
