import pytest
import torch
from pytest_mock import MockerFixture

from tests.helpers.mark import hardware_test
from vllm_omni.diffusion.models.flux2.flux2_transformer import (
    Flux2PosEmbed,
    Flux2Transformer2DModel,
)


# Initialize TP group before tests
@pytest.fixture(scope="function", autouse=True)
def setup_tp_group(mocker: MockerFixture):
    """Set up TP group for each test function"""
    mocker.patch(
        "vllm.model_executor.layers.linear.get_tensor_model_parallel_world_size",
        return_value=2,
    )
    mock_get_tp_group = mocker.patch("vllm.distributed.parallel_state.get_tp_group")
    mock_tp_group = mocker.MagicMock()
    mock_tp_group.world_size = 2
    mock_get_tp_group.return_value = mock_tp_group
    yield


class TestFlux2TransformerWeightLoading:
    """Test Flux2Transformer weight loading functionality"""

    @pytest.mark.core_model
    @hardware_test(res={"cuda": "L4"}, num_cards=1)
    def test_weight_loading_tp2(self, setup_tp_group):
        """Verify weights load correctly with TP=2"""
        # Prepare test data
        model = Flux2Transformer2DModel(
            num_layers=2,
            num_single_layers=1,
            num_attention_heads=48,
            attention_head_dim=128,
            joint_attention_dim=15360,
        )

        # Mock TP=2 weight loading
        mock_weights = []

        # 1. Test regular weight loading
        mock_weights.append(("x_embedder.weight", torch.randn(6144, 128)))
        mock_weights.append(("context_embedder.weight", torch.randn(6144, 15360)))
        mock_weights.append(("proj_out.weight", torch.randn(128, 6144)))

        # 2. Test stacked_params_mapping weight loading
        # Full weights - load_weights handles sharding internally
        to_qkv_weight = torch.randn(18432, 6144)  # Full weights
        mock_weights.append(("transformer_blocks.0.attn.to_qkv.weight", to_qkv_weight))

        # Add_kv_proj weights
        add_kv_proj_weight = torch.randn(18432, 6144)  # Full weights
        mock_weights.append(("transformer_blocks.0.attn.add_kv_proj.weight", add_kv_proj_weight))

        # 3. Test single block weight loading
        single_block_qkv_mlp_weight = torch.randn(18432 + 18432 * 3, 6144)  # Full weights
        mock_weights.append(("single_transformer_blocks.0.attn.to_qkv_mlp_proj.weight", single_block_qkv_mlp_weight))

        # Execute weight loading
        loaded_params = model.load_weights(mock_weights)

        # Verify
        assert len(loaded_params) > 0, "Parameters should be loaded"

        # Verify stacked_params_mapping is correctly set
        assert model.stacked_params_mapping is not None
        # Should have 6 mappings: 3 for to_qkv + 3 for add_kv_proj
        assert len(model.stacked_params_mapping) == 6, "Should have 6 mappings"

        # Verify weights are correctly loaded to corresponding layers
        assert hasattr(model.transformer_blocks[0].attn.to_qkv, "weight")
        # With TP=2, weight dimension on each GPU should be 18432/2 = 9216
        assert model.transformer_blocks[0].attn.to_qkv.weight.shape == (9216, 6144), (
            f"With TP=2, to_qkv weight dimension should be (9216, 6144), got {model.transformer_blocks[0].attn.to_qkv.weight.shape}"
        )


class TestFlux2RopePositionEmbedding:
    """Test Flux2 RoPE position embedding functionality"""

    @pytest.mark.core_model
    @pytest.mark.cpu
    def test_rope_position_embedding(self):
        """Verify RoPE produces correct embeddings for 4D coordinates"""
        # Prepare test data - use model default configuration
        # axes_dims_rope default is (32, 32, 32, 32)
        # get_1d_rotary_pos_embed outputs half the dimension for real/imag parts
        # So actual output dimension should be (16+16+16+16) = 64
        axes_dims = (32, 32, 32, 32)  # Use model default
        rope_theta = 2000  # Model default is 2000
        pos_embed = Flux2PosEmbed(theta=rope_theta, axes_dim=axes_dims)

        # Create test IDs
        seq_len = 10
        ids = torch.randint(0, 100, (seq_len, 4))  # [S, 4]

        # Forward pass
        freqs_cos, freqs_sin = pos_embed(ids)

        # Verify output shape - based on model config, expected dimension is 64
        # Each axes_dim=32 outputs 16-dim real part, sum of 4 dimensions = 64
        expected_dim = sum(axes_dims) // 2  # 128/2 = 64
        assert freqs_cos.shape == (seq_len, expected_dim), (
            f"Expected shape {(seq_len, expected_dim)}, got {freqs_cos.shape}"
        )
        assert freqs_sin.shape == (seq_len, expected_dim), (
            f"Expected shape {(seq_len, expected_dim)}, got {freqs_sin.shape}"
        )

        # Verify output type - NPU may use float32, other devices use float64
        assert freqs_cos.dtype in [torch.float32, torch.float64], "Should be float type"
        assert freqs_sin.dtype in [torch.float32, torch.float64], "Should be float type"

        # Verify value range
        assert torch.all(freqs_cos >= -1) and torch.all(freqs_cos <= 1), "cos values should be in [-1, 1]"
        assert torch.all(freqs_sin >= -1) and torch.all(freqs_sin <= 1), "sin values should be in [-1, 1]"

        # Verify trigonometric relationship: cos^2 + sin^2 ≈ 1
        cos_sq_sin_sq = freqs_cos**2 + freqs_sin**2
        assert torch.allclose(cos_sq_sin_sq, torch.ones_like(cos_sq_sin_sq), atol=1e-6), "cos^2 + sin^2 should ≈ 1"

        # Verify different positions produce different embeddings
        ids_diff = torch.randint(0, 100, (seq_len, 4))
        freqs_cos_diff, freqs_sin_diff = pos_embed(ids_diff)
        assert not torch.allclose(freqs_cos, freqs_cos_diff), "Different positions should produce different embeddings"

        # Verify same positions produce same embeddings
        ids_same = ids.clone()
        freqs_cos_same, freqs_sin_same = pos_embed(ids_same)
        assert torch.allclose(freqs_cos, freqs_cos_same), "Same positions should produce same embeddings"
        assert torch.allclose(freqs_sin, freqs_sin_same), "Same positions should produce same embeddings"


class TestFlux2PackedModuleMapping:
    """Test Flux2 packed module mapping functionality"""

    @pytest.mark.core_model
    @hardware_test(res={"cuda": "L4"}, num_cards=1)
    def test_packed_module_mapping(self, setup_tp_group):
        """Verify to_qkv packing matches HF checkpoint"""
        model = Flux2Transformer2DModel(
            num_layers=1,
            num_single_layers=0,
            num_attention_heads=48,
            attention_head_dim=128,
            joint_attention_dim=15360,
        )

        # Verify stacked_params_mapping is correctly initialized
        model.load_weights([])  # Trigger stacked_params_mapping initialization
        assert model.stacked_params_mapping is not None

        # Verify mapping configuration
        expected_mappings = [
            (".to_qkv.", ".to_q.", "q"),
            (".to_qkv.", ".to_k.", "k"),
            (".to_qkv.", ".to_v.", "v"),
            (".add_kv_proj", ".add_q_proj", "q"),
            (".add_kv_proj", ".add_k_proj", "k"),
            (".add_kv_proj", ".add_v_proj", "v"),
        ]
        assert model.stacked_params_mapping == expected_mappings

        # Create mock HF checkpoint weights
        hf_weights = []

        # Mock HF separated Q/K/V weights
        attn_block = model.transformer_blocks[0].attn
        head_dim = 128
        num_heads = 48
        hidden_size = 6144
        # Full weight dimension
        full_shard_size = num_heads * head_dim  # 6144

        # Q projection weights (full weights)
        q_weight = torch.randn(full_shard_size, hidden_size)
        hf_weights.append(("transformer_blocks.0.attn.to_q.weight", q_weight))

        # K projection weights (full weights)
        k_weight = torch.randn(full_shard_size, hidden_size)
        hf_weights.append(("transformer_blocks.0.attn.to_k.weight", k_weight))

        # V projection weights (full weights)
        v_weight = torch.randn(full_shard_size, hidden_size)
        hf_weights.append(("transformer_blocks.0.attn.to_v.weight", v_weight))

        # Mock HF separated add_q/k/v projection weights
        add_q_weight = torch.randn(full_shard_size, hidden_size)
        hf_weights.append(("transformer_blocks.0.attn.add_q_proj.weight", add_q_weight))

        add_k_weight = torch.randn(full_shard_size, hidden_size)
        hf_weights.append(("transformer_blocks.0.attn.add_k_proj.weight", add_k_weight))

        add_v_weight = torch.randn(full_shard_size, hidden_size)
        hf_weights.append(("transformer_blocks.0.attn.add_v_proj.weight", add_v_weight))

        # Execute weight loading
        loaded_params = model.load_weights(hf_weights)

        # Verify weights are loaded
        assert len(loaded_params) > 0

        # Verify final QKV weights are correctly combined (considering TP sharding)
        # With TP=2, dimension on each GPU should be half of full dimension
        expected_qkv_shape = (full_shard_size * 3 // 2, hidden_size)  # 18432/2 = 9216
        assert attn_block.to_qkv.weight.shape == expected_qkv_shape, (
            f"to_qkv weight dimension should be {expected_qkv_shape}, got {attn_block.to_qkv.weight.shape}"
        )

        expected_add_kv_shape = (full_shard_size * 3 // 2, hidden_size)
        assert attn_block.add_kv_proj.weight.shape == expected_add_kv_shape, (
            f"add_kv_proj weight dimension should be {expected_add_kv_shape}, got {attn_block.add_kv_proj.weight.shape}"
        )

    @pytest.mark.core_model
    @hardware_test(res={"cuda": "L4"}, num_cards=1)
    def test_packed_mapping_edge_cases(self, setup_tp_group):
        """Test edge cases for packed mapping"""
        model = Flux2Transformer2DModel(
            num_layers=1,
            num_single_layers=1,
            num_attention_heads=48,
            attention_head_dim=128,
            joint_attention_dim=15360,
        )
        model.load_weights([])

        # Test invalid weight names
        invalid_weights = [("invalid.weight", torch.randn(10, 10))]
        loaded_params = model.load_weights(invalid_weights)
        assert len(loaded_params) == 0, "Should not load invalid weights"

        # Test to_out weight renaming
        to_out_weight = torch.randn(6144, 6144)
        weights = [("transformer_blocks.0.attn.to_out.0.weight", to_out_weight)]
        loaded_params = model.load_weights(weights)

        # Check if to_out related weights are loaded
        to_out_loaded = any("to_out" in p for p in loaded_params)
        assert to_out_loaded, "to_out weights should be correctly renamed and loaded"

        # Test partial weight loading
        partial_weights = [
            ("x_embedder.weight", torch.randn(6144, 128)),
            ("transformer_blocks.0.attn.to_q.weight", torch.randn(6144, 6144)),  # Full weights
        ]
        loaded_params = model.load_weights(partial_weights)
        assert len(loaded_params) == 2, "Should load two weights"
        assert "x_embedder.weight" in loaded_params
        assert any("to_qkv" in p for p in loaded_params), "to_q should be mapped to to_qkv"
