"""Tests for MLX RRDBNet (RealESRGAN) super-resolution model."""

import os
import sys

import mlx.core as mx
import numpy as np
import pytest

# Add hy3dpaint to path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

from utils.image_super_utils_mlx import (
    RRDBNetMLX,
    ResidualDenseBlock,
    RRDB,
    _nearest_upsample_2x,
    imageSuperNetMLX,
)


# ---------------------------------------------------------------------------
# Architecture tests
# ---------------------------------------------------------------------------


class TestResidualDenseBlock:
    """Tests for the ResidualDenseBlock module."""

    def test_creation(self):
        rdb = ResidualDenseBlock(num_feat=64, num_grow_ch=32)
        assert rdb.conv1 is not None
        assert rdb.conv5 is not None

    def test_forward_shape(self):
        rdb = ResidualDenseBlock(num_feat=64, num_grow_ch=32)
        x = mx.zeros((1, 16, 16, 64))
        out = rdb(x)
        mx.eval(out)
        assert out.shape == (1, 16, 16, 64), f"Expected (1,16,16,64), got {out.shape}"

    def test_residual_connection(self):
        """Output should differ from input (conv weights are random)."""
        rdb = ResidualDenseBlock(num_feat=64, num_grow_ch=32)
        x = mx.ones((1, 4, 4, 64))
        out = rdb(x)
        mx.eval(out)
        # The residual path adds x, so output should not be zero
        assert not mx.allclose(out, mx.zeros_like(out)).item()


class TestRRDB:
    """Tests for the RRDB module."""

    def test_creation(self):
        rrdb = RRDB(num_feat=64, num_grow_ch=32)
        assert len(rrdb.rdb1.conv1.weight.shape) == 4

    def test_forward_shape(self):
        rrdb = RRDB(num_feat=64, num_grow_ch=32)
        x = mx.zeros((1, 8, 8, 64))
        out = rrdb(x)
        mx.eval(out)
        assert out.shape == (1, 8, 8, 64), f"Expected (1,8,8,64), got {out.shape}"


class TestRRDBNetMLX:
    """Tests for the full RRDBNet model."""

    def test_creation_default(self):
        model = RRDBNetMLX()
        assert model.scale == 4
        assert len(model.body) == 23

    def test_creation_custom(self):
        model = RRDBNetMLX(num_in_ch=3, num_out_ch=3, num_feat=32, num_block=4, num_grow_ch=16, scale=4)
        assert len(model.body) == 4

    def test_forward_shape_scale4(self):
        """Input 64x64x3 should produce 256x256x3 with scale=4."""
        model = RRDBNetMLX(num_in_ch=3, num_out_ch=3, num_feat=32, num_block=2, num_grow_ch=16, scale=4)
        x = mx.zeros((1, 64, 64, 3))
        out = model(x)
        mx.eval(out)
        assert out.shape == (1, 256, 256, 3), f"Expected (1,256,256,3), got {out.shape}"

    def test_forward_shape_scale2(self):
        """Input 64x64x3 should produce 128x128x3 with scale=2."""
        model = RRDBNetMLX(num_in_ch=3, num_out_ch=3, num_feat=32, num_block=2, num_grow_ch=16, scale=2)
        x = mx.zeros((1, 64, 64, 3))
        out = model(x)
        mx.eval(out)
        assert out.shape == (1, 128, 128, 3), f"Expected (1,128,128,3), got {out.shape}"

    def test_forward_nonzero_input(self):
        """Model should produce non-zero output for non-zero input."""
        model = RRDBNetMLX(num_in_ch=3, num_out_ch=3, num_feat=32, num_block=2, num_grow_ch=16, scale=4)
        x = mx.ones((1, 16, 16, 3)) * 0.5
        out = model(x)
        mx.eval(out)
        assert out.shape == (1, 64, 64, 3)
        assert not mx.allclose(out, mx.zeros_like(out)).item()


# ---------------------------------------------------------------------------
# Utility tests
# ---------------------------------------------------------------------------


class TestNearestUpsample:
    """Tests for the nearest-neighbor upsampling function."""

    def test_2x_upsample_shape(self):
        x = mx.zeros((1, 8, 8, 3))
        out = _nearest_upsample_2x(x)
        mx.eval(out)
        assert out.shape == (1, 16, 16, 3)

    def test_2x_upsample_values(self):
        """Each pixel should be replicated in a 2x2 block."""
        x = mx.array([[[[1.0, 2.0, 3.0]]]])  # (1, 1, 1, 3)
        out = _nearest_upsample_2x(x)
        mx.eval(out)
        assert out.shape == (1, 2, 2, 3)
        expected = mx.array([[[[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]],
                              [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]]]])
        assert mx.allclose(out, expected).item()


class TestPixelUnshuffle:
    """Tests for the pixel_unshuffle static method."""

    def test_unshuffle_scale2(self):
        x = mx.zeros((1, 8, 8, 3))
        out = RRDBNetMLX._pixel_unshuffle(x, 2)
        mx.eval(out)
        assert out.shape == (1, 4, 4, 12)

    def test_unshuffle_scale4(self):
        x = mx.zeros((1, 16, 16, 3))
        out = RRDBNetMLX._pixel_unshuffle(x, 4)
        mx.eval(out)
        assert out.shape == (1, 4, 4, 48)


# ---------------------------------------------------------------------------
# Weight loading test (conditional on weight file availability)
# ---------------------------------------------------------------------------


def _resolve_realesrgan_weights():
    """Local override, else fetch from HF (same repo as the paint model)."""
    env = os.environ.get("REALESRGAN_MLX_WEIGHTS")
    if env and os.path.exists(env):
        return env
    try:
        from huggingface_hub import hf_hub_download
        return hf_hub_download(
            repo_id="dgrauet/hunyuan3d-2.1-mlx",
            filename="realesrgan_x4plus.safetensors",
        )
    except Exception:
        return None


WEIGHTS_PATH = _resolve_realesrgan_weights()


@pytest.mark.skipif(WEIGHTS_PATH is None, reason="Could not resolve RealESRGAN MLX weights (no env var, HF download failed)")
class TestWeightLoading:
    """Tests that require converted MLX weights."""

    def test_load_weights(self):
        wrapper = imageSuperNetMLX(WEIGHTS_PATH)
        assert wrapper.model is not None

    def test_inference_with_weights(self):
        from PIL import Image

        wrapper = imageSuperNetMLX(WEIGHTS_PATH)
        # Create a small test image
        img = Image.fromarray(np.random.randint(0, 255, (32, 32, 3), dtype=np.uint8))
        out = wrapper(img)
        assert out.size == (128, 128), f"Expected (128,128), got {out.size}"
