# Copyright 2026 OpenBMB and the HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import math
import unittest

import numpy as np

from transformers.image_utils import get_image_size
from transformers.testing_utils import require_torch, require_vision
from transformers.utils import is_torch_available, is_vision_available

from ...test_image_processing_common import ImageProcessingTester, ImageProcessingTestMixin


if is_torch_available():
    import torch

if is_vision_available():
    from PIL import Image


class MiniCPMV4_6ImageProcessingTester(ImageProcessingTester):
    def __init__(self, **kwargs):
        # Random test inputs kwargs
        kwargs.setdefault("batch_size", 2)
        kwargs.setdefault("min_resolution", 64)
        kwargs.setdefault("max_resolution", 128)

        # Image processor init kwargs
        kwargs.setdefault("scale_resolution", 448)
        kwargs.setdefault("patch_size", 14)

        super().__init__(**kwargs)

    def expected_output_image_shape(self, image_inputs):
        """Return the expected NaViT-packed shape [C, P, total_L] for pixel_values[0]."""
        total_L = 0
        for image in image_inputs:
            if isinstance(image, Image.Image):
                height, width = image.height, image.width
            else:
                height, width = get_image_size(image)

            aspect_ratio = width / height
            new_height = int(self.scale_resolution / math.sqrt(aspect_ratio))
            new_width = int(new_height * aspect_ratio)

            divisor = self.patch_size * 4
            best_height = max(round(new_height / divisor) * divisor, divisor)
            best_width = max(round(new_width / divisor) * divisor, divisor)

            total_L += best_height * best_width // self.patch_size

        return [self.num_channels, self.patch_size, total_L]


@require_torch
@require_vision
class MiniCPMV4_6ImageProcessingTest(ImageProcessingTestMixin, unittest.TestCase):
    image_processor_tester_class = MiniCPMV4_6ImageProcessingTester

    def test_call_pil(self):
        for image_processing_class in self.image_processing_classes.values():
            image_processing = image_processing_class(**self.image_processor_dict)
            image_inputs = self.image_processor_tester.prepare_image_inputs(equal_resolution=False)
            for image in image_inputs:
                self.assertIsInstance(image, Image.Image)

            # Test not batched input
            encoded_images = image_processing(image_inputs[0], return_tensors="pt").pixel_values
            expected_shape = self.image_processor_tester.expected_output_image_shape([image_inputs[0]])
            self.assertEqual(tuple(encoded_images.shape), (1, *expected_shape))

            # Test batched
            encoded_images = image_processing(image_inputs, return_tensors="pt").pixel_values
            expected_shape = self.image_processor_tester.expected_output_image_shape(image_inputs)
            self.assertEqual(tuple(encoded_images.shape), (1, *expected_shape))

    def test_call_numpy(self):
        for image_processing_class in self.image_processing_classes.values():
            image_processing = image_processing_class(**self.image_processor_dict)
            image_inputs = self.image_processor_tester.prepare_image_inputs(equal_resolution=False, numpify=True)
            for image in image_inputs:
                self.assertIsInstance(image, np.ndarray)

            # Test not batched input
            encoded_images = image_processing(image_inputs[0], return_tensors="pt").pixel_values
            expected_shape = self.image_processor_tester.expected_output_image_shape([image_inputs[0]])
            self.assertEqual(tuple(encoded_images.shape), (1, *expected_shape))

            # Test batched
            encoded_images = image_processing(image_inputs, return_tensors="pt").pixel_values
            expected_shape = self.image_processor_tester.expected_output_image_shape(image_inputs)
            self.assertEqual(tuple(encoded_images.shape), (1, *expected_shape))

    def test_call_pytorch(self):
        for image_processing_class in self.image_processing_classes.values():
            image_processing = image_processing_class(**self.image_processor_dict)
            image_inputs = self.image_processor_tester.prepare_image_inputs(equal_resolution=False, torchify=True)
            for image in image_inputs:
                self.assertIsInstance(image, torch.Tensor)

            # Test not batched input
            encoded_images = image_processing(image_inputs[0], return_tensors="pt").pixel_values
            expected_shape = self.image_processor_tester.expected_output_image_shape([image_inputs[0]])
            self.assertEqual(tuple(encoded_images.shape), (1, *expected_shape))

            # Test batched
            encoded_images = image_processing(image_inputs, return_tensors="pt").pixel_values
            expected_shape = self.image_processor_tester.expected_output_image_shape(image_inputs)
            self.assertEqual(tuple(encoded_images.shape), (1, *expected_shape))

    @unittest.skip("NaViT expected_output_image_shape cannot infer channel dim for 4-channel images")
    def test_call_numpy_4_channels(self):
        pass

    def test_call_returns_expected_keys(self):
        for image_processing_class in self.image_processing_classes.values():
            image_processor = image_processing_class(**self.image_processor_dict)
            images = self.image_processor_tester.prepare_image_inputs(torchify=True)
            result = image_processor(images, return_tensors="pt")
            self.assertIn("pixel_values", result)
            self.assertIn("target_sizes", result)
            self.assertIn("num_patches_per_image", result)
            self.assertIn("grids", result)

    def test_pixel_values_are_tensors(self):
        for image_processing_class in self.image_processing_classes.values():
            image_processor = image_processing_class(**self.image_processor_dict)
            images = self.image_processor_tester.prepare_image_inputs(torchify=True)
            result = image_processor(images, return_tensors="pt")
            for pv in result["pixel_values"]:
                self.assertIsInstance(pv, torch.Tensor)

    def test_downsample_modes(self):
        for image_processing_class in self.image_processing_classes.values():
            images = self.image_processor_tester.prepare_image_inputs(equal_resolution=True, torchify=True)

            ip_16x = image_processing_class(**{**self.image_processor_dict, "downsample_mode": "16x"})
            result_16x = ip_16x(images, return_tensors="pt")

            ip_4x = image_processing_class(**{**self.image_processor_dict, "downsample_mode": "4x"})
            result_4x = ip_4x(images, return_tensors="pt")

            for ts_4x, ts_16x in zip(result_4x["target_sizes"], result_16x["target_sizes"]):
                h4, w4 = (ts_4x[0], ts_4x[1]) if not hasattr(ts_4x[0], "item") else (ts_4x[0].item(), ts_4x[1].item())
                h16, w16 = (
                    (ts_16x[0], ts_16x[1]) if not hasattr(ts_16x[0], "item") else (ts_16x[0].item(), ts_16x[1].item())
                )
                self.assertGreaterEqual(h4 * w4, h16 * w16)

    def test_crop_to_patches_aspect_ratio(self):
        """Test that row/column ordering is correct when cropping non-square images to patches"""
        for image_processing_class in self.image_processing_classes.values():
            patch_size = 14
            image_processor = image_processing_class(slice_mode=True, max_slice_nums=7, scale_resolution=10)

            for num_rows, num_cols in [(2, 3), (3, 2), (1, 6), (6, 1)]:
                image_height = patch_size * num_rows  # 128
                image_width = patch_size * num_cols  # 192
                test_image = Image.new("RGB", (image_width, image_height))
                result = image_processor(test_image, return_tensors="pt")

                # Should produce 7 patches (6 grid patches + 1 thumbnail)
                self.assertEqual(result.grids, [[num_rows, num_cols]])
                self.assertEqual(tuple(result.pixel_values.shape), (1, 3, patch_size, 1568))
