"""`acaua.io.load_images` — input coercion across the whole accepted Union.

Every accepted input type should reach a PIL RGB image. Every rejected
input type should raise `InputFormatError` with a helpful message.
"""

from __future__ import annotations

import io as _io
from pathlib import Path
from unittest.mock import Mock, patch

import numpy as np
import pytest
import torch
from PIL import Image

from acaua.exceptions import InputFormatError, WeightDownloadError
from acaua.io import _bgr_smell, load_images


class TestAcceptedInputs:
    def test_pil_image_returns_unchanged(self, rgb_pil: Image.Image) -> None:
        out, was_seq = load_images(rgb_pil)
        assert was_seq is False
        assert len(out) == 1
        assert out[0].mode == "RGB"

    def test_pil_non_rgb_is_converted(self) -> None:
        grey = Image.new("L", (32, 32), 128)
        out, _ = load_images(grey)
        assert out[0].mode == "RGB"

    def test_path_string(self, rgb_pil: Image.Image, tmp_path: Path) -> None:
        p = tmp_path / "img.png"
        rgb_pil.save(p)
        out, was_seq = load_images(str(p))
        assert was_seq is False
        assert out[0].size == rgb_pil.size

    def test_pathlib_path(self, rgb_pil: Image.Image, tmp_path: Path) -> None:
        p = tmp_path / "img.jpg"
        rgb_pil.save(p)
        out, _ = load_images(p)
        assert out[0].mode == "RGB"

    def test_ndarray_hwc_uint8_rgb(self, rgb_ndarray: np.ndarray) -> None:
        out, was_seq = load_images(rgb_ndarray)
        assert was_seq is False
        assert out[0].size == (64, 64)
        assert out[0].mode == "RGB"

    def test_tensor_chw_float32(self, rgb_tensor: torch.Tensor) -> None:
        out, was_seq = load_images(rgb_tensor)
        assert was_seq is False
        assert out[0].size == (64, 64)
        assert out[0].mode == "RGB"

    def test_sequence_returns_list(self, rgb_pil: Image.Image) -> None:
        out, was_seq = load_images([rgb_pil, rgb_pil, rgb_pil])
        assert was_seq is True
        assert len(out) == 3

    def test_tuple_is_also_a_sequence(self, rgb_pil: Image.Image) -> None:
        out, was_seq = load_images((rgb_pil, rgb_pil))
        assert was_seq is True
        assert len(out) == 2

    def test_mixed_type_sequence(
        self, rgb_pil: Image.Image, rgb_ndarray: np.ndarray, rgb_tensor: torch.Tensor
    ) -> None:
        out, was_seq = load_images([rgb_pil, rgb_ndarray, rgb_tensor])
        assert was_seq is True
        assert len(out) == 3
        assert all(im.mode == "RGB" for im in out)


class TestRejectedInputs:
    def test_empty_sequence(self) -> None:
        with pytest.raises(InputFormatError, match="empty"):
            load_images([])

    def test_ndarray_wrong_ndim(self) -> None:
        arr = np.zeros((64, 64), dtype=np.uint8)
        with pytest.raises(InputFormatError, match="HWC"):
            load_images(arr)

    def test_ndarray_wrong_channel_count(self) -> None:
        arr = np.zeros((64, 64, 4), dtype=np.uint8)
        with pytest.raises(InputFormatError, match="3 channels"):
            load_images(arr)

    def test_ndarray_wrong_dtype(self) -> None:
        arr = np.zeros((64, 64, 3), dtype=np.float32)
        with pytest.raises(InputFormatError, match="uint8"):
            load_images(arr)

    def test_tensor_wrong_ndim(self) -> None:
        t = torch.zeros(64, 64, dtype=torch.float32)
        with pytest.raises(InputFormatError, match="CHW"):
            load_images(t)

    def test_tensor_wrong_channel_count(self) -> None:
        t = torch.zeros(4, 64, 64, dtype=torch.float32)
        with pytest.raises(InputFormatError, match="CHW"):
            load_images(t)

    def test_tensor_wrong_dtype(self) -> None:
        t = torch.zeros(3, 64, 64, dtype=torch.int64)
        with pytest.raises(InputFormatError, match="float32"):
            load_images(t)

    def test_unsupported_type(self) -> None:
        with pytest.raises(InputFormatError, match="unsupported input type"):
            load_images(42)  # type: ignore[arg-type]


class TestBGRHeuristic:
    def test_bgr_smell_trips_by_default(self, bgr_smelling_ndarray: np.ndarray) -> None:
        with pytest.raises(InputFormatError, match="BGR"):
            load_images(bgr_smelling_ndarray)

    def test_strict_rgb_false_allows_bgr(self, bgr_smelling_ndarray: np.ndarray) -> None:
        out, _ = load_images(bgr_smelling_ndarray, strict_rgb=False)
        assert out[0].mode == "RGB"

    def test_rgb_with_warm_tones_does_not_trip(self) -> None:
        # R=120, G=80, B=110 → diff(R,B)=10 → below threshold (>10 required).
        arr = np.stack(
            [
                np.full((32, 32), 120, dtype=np.uint8),
                np.full((32, 32), 80, dtype=np.uint8),
                np.full((32, 32), 110, dtype=np.uint8),
            ],
            axis=-1,
        )
        out, _ = load_images(arr)
        assert out[0].mode == "RGB"

    def test_bgr_smell_function_direct(self, bgr_smelling_ndarray: np.ndarray) -> None:
        assert _bgr_smell(bgr_smelling_ndarray) is True

    def test_bgr_smell_on_wrong_shape_returns_false(self) -> None:
        assert _bgr_smell(np.zeros((64, 64), dtype=np.uint8)) is False
        assert _bgr_smell(np.zeros((64, 64, 4), dtype=np.uint8)) is False
        assert _bgr_smell(np.zeros((64, 64, 3), dtype=np.float32)) is False


class TestURLLoading:
    def test_http_url_downloads_and_opens(self, rgb_pil: Image.Image) -> None:
        buf = _io.BytesIO()
        rgb_pil.save(buf, format="PNG")
        mock_response = Mock()
        mock_response.content = buf.getvalue()
        mock_response.raise_for_status = Mock()
        with patch("requests.get", return_value=mock_response) as mock_get:
            out, _ = load_images("https://example.com/img.png")
            mock_get.assert_called_once()
            assert out[0].mode == "RGB"

    def test_https_url_is_recognized(self, rgb_pil: Image.Image) -> None:
        buf = _io.BytesIO()
        rgb_pil.save(buf, format="PNG")
        mock_response = Mock(content=buf.getvalue())
        mock_response.raise_for_status = Mock()
        with patch("requests.get", return_value=mock_response) as mock_get:
            load_images("http://example.com/img.png")
            mock_get.assert_called_once()

    def test_url_network_failure_wraps(self) -> None:
        import requests as real_requests

        def boom(*a: object, **k: object) -> None:
            raise real_requests.ConnectionError("no net")

        with (
            patch("requests.get", side_effect=boom),
            pytest.raises(WeightDownloadError, match="URL"),
        ):
            load_images("https://example.com/img.png")

    def test_non_url_string_is_path(self, tmp_path: Path, rgb_pil: Image.Image) -> None:
        p = tmp_path / "local.png"
        rgb_pil.save(p)
        out, _ = load_images(str(p))
        assert out[0].mode == "RGB"
