# ------------------------------------------------------------------------
# RF-DETR
# Copyright (c) 2025 Roboflow. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
# ------------------------------------------------------------------------
"""Tests that unoptimized inference always runs the module in eval mode."""

from types import SimpleNamespace

import PIL.Image
import pytest
import torch

from rfdetr import detr as detr_module

from .helpers import _BaseFakeRFDETR


class _FakeModelWithDropout(torch.nn.Module):
    """Minimal module whose behavior differs between train and eval mode."""

    def __init__(self) -> None:
        super().__init__()
        self.dropout = torch.nn.Dropout(p=0.5)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """Pass input through dropout, active only in train mode."""
        return self.dropout(x)


class _FakeModelContext:
    """Minimal model context supplying the attributes predict() and train() need."""

    def __init__(self) -> None:
        self.device = torch.device("cpu")
        self.resolution = 28
        self.model = _FakeModelWithDropout()
        self.inference_model = None


class _FakeRFDETR(_BaseFakeRFDETR):
    """Concrete test double: provides a dropout-bearing model for eval-mode tests."""

    def get_model(self, config: SimpleNamespace, *, trust_checkpoint: bool = False) -> _FakeModelContext:
        """Return a minimal model context with a dropout-bearing module."""
        return _FakeModelContext()


class TestUnoptimizedInferenceEvalMode:
    """`_ensure_eval_mode_for_unoptimized_inference` must keep the module in eval mode."""

    def test_eval_mode_reasserted_after_train_round_trip(self) -> None:
        """Eval mode must be applied to whatever self.model.model currently points to.

        ``train()`` rebinds ``self.model.model`` to a brand-new module left in training mode, so eval must be re-applied
        to the *current* object on every call — not to a cached reference captured at init.
        """
        rfdetr = _FakeRFDETR()

        # First inference call: warns once and switches to eval mode.
        rfdetr._ensure_eval_mode_for_unoptimized_inference()
        assert rfdetr.model.model.training is False

        # Simulate train() rebinding self.model.model to a fresh training-mode module.
        rfdetr.model.model = _FakeModelWithDropout()
        assert rfdetr.model.model.training is True  # new object starts in train mode

        # Every subsequent inference call must re-assert eval on the *new* object.
        rfdetr._ensure_eval_mode_for_unoptimized_inference()
        assert rfdetr.model.model.training is False

    def test_optimized_model_skips_eval_assertion(self) -> None:
        """When _is_optimized_for_inference is True, the method must be a no-op.

        The compiled inference_model snapshot is already in eval mode; calling eval() on the stale self.model.model
        would target the wrong object.
        """
        rfdetr = _FakeRFDETR()
        rfdetr._is_optimized_for_inference = True
        rfdetr.model.model.train()
        assert rfdetr.model.model.training is True  # confirm starting state

        rfdetr._ensure_eval_mode_for_unoptimized_inference()

        assert rfdetr.model.model.training is True  # must remain unchanged

    def test_not_optimized_warning_emitted_only_once(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """The not-optimized warning is logged at most once across repeated calls."""
        warnings: list[str] = []
        monkeypatch.setattr(detr_module.logger, "warning", lambda msg, *a, **k: warnings.append(msg))

        rfdetr = _FakeRFDETR()
        rfdetr._ensure_eval_mode_for_unoptimized_inference()
        rfdetr.model.model.train()
        rfdetr._ensure_eval_mode_for_unoptimized_inference()
        rfdetr._ensure_eval_mode_for_unoptimized_inference()

        assert len(warnings) == 1

    def test_eval_mode_applied_on_every_call(self) -> None:
        """Eval() must run on every call, not just when the warning fires.

        Simulate the code path where the warning has already been emitted
        (``_has_warned_about_not_being_optimized_for_inference=True``) and verify
        that ``eval()`` is still applied to the current module.
        """
        rfdetr = _FakeRFDETR()
        rfdetr._has_warned_about_not_being_optimized_for_inference = True
        rfdetr.model.model.train()

        rfdetr._ensure_eval_mode_for_unoptimized_inference()

        assert rfdetr.model.model.training is False

    def test_predict_puts_module_in_eval_mode(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """Predict() must delegate to _ensure_eval_mode_for_unoptimized_inference, leaving module in eval mode."""
        rfdetr = _FakeRFDETR()
        img = PIL.Image.new("RGB", (640, 640), color=(128, 128, 128))

        monkeypatch.setattr(
            rfdetr.model.model,
            "forward",
            lambda batch: {"pred_logits": torch.zeros(1, 10, 81), "pred_boxes": torch.zeros(1, 10, 4)},
        )
        monkeypatch.setattr(
            rfdetr.model,
            "postprocess",
            lambda preds, target_sizes, score_threshold=None: [
                {"scores": torch.zeros(0), "labels": torch.zeros(0, dtype=torch.long), "boxes": torch.zeros(0, 4)}
            ],
            raising=False,
        )

        rfdetr.predict(img)

        assert rfdetr.model.model.training is False
