"""Tests for WeightUpdater."""

from __future__ import annotations

import json
from pathlib import Path  # noqa: TC003 — needed at runtime for pytest fixtures
from unittest.mock import MagicMock, patch

import httpx

from llmhosts.training.registry import WeightRegistry
from llmhosts.training.updater import UpdateResult, WeightUpdater


def _make_registry(tmp_path: Path) -> WeightRegistry:
    return WeightRegistry(registry_dir=tmp_path / "registry")


class TestWeightUpdater:
    def test_returns_not_updated_on_404(self, tmp_path: Path) -> None:
        """HTTP 404 from registry URL should return updated=False."""
        registry = _make_registry(tmp_path)
        updater = WeightUpdater(registry=registry)

        with patch("httpx.get") as mock_get:
            response = MagicMock()
            response.raise_for_status.side_effect = httpx.HTTPStatusError(
                "404 Not Found",
                request=MagicMock(),
                response=MagicMock(status_code=404),
            )
            mock_get.return_value = response

            result = updater.check_and_update(registry_url="http://example.com/manifest.json")

        assert isinstance(result, UpdateResult)
        assert result.updated is False
        assert result.version is None
        assert len(result.message) > 0

    def test_returns_not_updated_on_timeout(self, tmp_path: Path) -> None:
        """Network timeout should return updated=False."""
        registry = _make_registry(tmp_path)
        updater = WeightUpdater(registry=registry)

        with patch("httpx.get") as mock_get:
            mock_get.side_effect = httpx.TimeoutException("timeout")

            result = updater.check_and_update(registry_url="http://example.com/manifest.json")

        assert result.updated is False
        assert result.version is None
        assert "timeout" in result.message.lower() or "Timeout" in result.message

    def test_returns_not_updated_on_malformed_json(self, tmp_path: Path) -> None:
        """Malformed JSON response should return updated=False."""
        registry = _make_registry(tmp_path)
        updater = WeightUpdater(registry=registry)

        with patch("httpx.get") as mock_get:
            response = MagicMock()
            response.raise_for_status.return_value = None
            response.json.side_effect = json.JSONDecodeError("bad json", "", 0)
            mock_get.return_value = response

            result = updater.check_and_update(registry_url="http://example.com/manifest.json")

        assert result.updated is False
        assert result.version is None

    def test_returns_not_updated_when_remote_version_older(self, tmp_path: Path) -> None:
        """When local version is newer than remote, should return updated=False."""
        registry = _make_registry(tmp_path)
        # Pre-register a "newer" local version
        from llmhosts.training.trainer import TrainingResult

        adapter_dir = tmp_path / "adapter"
        adapter_dir.mkdir()
        (adapter_dir / "w.bin").write_bytes(b"x")
        result = TrainingResult(
            weights_path=adapter_dir,
            accuracy=0.88,
            latency_ms=20.0,
            n_samples=10_000,
            epochs_completed=3,
            delta_size_mb=1.0,
        )
        # Register with a very new version tag and activate it
        registry.register(result, version="v99991231-10k")
        registry.activate("v99991231-10k")

        updater = WeightUpdater(registry=registry)

        remote_manifest = [
            {
                "version": "v20260101-5k",
                "created_at": "2026-01-01T00:00:00Z",
                "accuracy": 0.80,
                "n_training_samples": 5000,
                "weights_url": "http://example.com/weights.bin",
            }
        ]

        with patch("httpx.get") as mock_get:
            response = MagicMock()
            response.raise_for_status.return_value = None
            response.json.return_value = remote_manifest
            mock_get.return_value = response

            result_check = updater.check_and_update(registry_url="http://example.com/manifest.json")

        assert result_check.updated is False

    def test_returns_updated_when_remote_version_newer(self, tmp_path: Path) -> None:
        """When remote version is newer than local, should download and return updated=True."""
        registry = _make_registry(tmp_path)
        updater = WeightUpdater(registry=registry)

        remote_manifest = [
            {
                "version": "v20260224-10k",
                "created_at": "2026-02-24T00:00:00Z",
                "accuracy": 0.88,
                "n_training_samples": 10_000,
                "weights_url": "http://example.com/adapter.bin",
                "base_model": "answerdotai/ModernBERT-base",
                "lora_rank": 16,
                "lora_alpha": 32,
            }
        ]

        # Mock the streaming download as well
        with (
            patch("httpx.get") as mock_get,
            patch("httpx.stream") as mock_stream,
        ):
            # Mock manifest fetch
            manifest_response = MagicMock()
            manifest_response.raise_for_status.return_value = None
            manifest_response.json.return_value = remote_manifest
            mock_get.return_value = manifest_response

            # Mock streaming weights download
            stream_response = MagicMock()
            stream_response.__enter__ = MagicMock(return_value=stream_response)
            stream_response.__exit__ = MagicMock(return_value=False)
            stream_response.raise_for_status.return_value = None
            stream_response.iter_bytes.return_value = iter([b"fake_weights_data"])
            mock_stream.return_value = stream_response

            result = updater.check_and_update(registry_url="http://example.com/manifest.json")

        assert result.updated is True
        assert result.version == "v20260224-10k"

    def test_returns_not_updated_on_empty_manifest(self, tmp_path: Path) -> None:
        """Empty manifest list should return updated=False."""
        registry = _make_registry(tmp_path)
        updater = WeightUpdater(registry=registry)

        with patch("httpx.get") as mock_get:
            response = MagicMock()
            response.raise_for_status.return_value = None
            response.json.return_value = []
            mock_get.return_value = response

            result = updater.check_and_update(registry_url="http://example.com/manifest.json")

        assert result.updated is False

    def test_returns_not_updated_on_connection_error(self, tmp_path: Path) -> None:
        """Connection error should return updated=False without raising."""
        registry = _make_registry(tmp_path)
        updater = WeightUpdater(registry=registry)

        with patch("httpx.get") as mock_get:
            mock_get.side_effect = httpx.ConnectError("connection refused")

            result = updater.check_and_update(registry_url="http://localhost:9999/manifest.json")

        assert result.updated is False
        assert result.version is None

    def test_uses_default_registry_url_when_none(self, tmp_path: Path) -> None:
        """When registry_url is None, should use the HuggingFace default URL."""
        registry = _make_registry(tmp_path)
        updater = WeightUpdater(registry=registry)

        captured_urls: list[str] = []

        def capture_get(url: str, **kwargs: object) -> MagicMock:
            captured_urls.append(url)
            raise httpx.ConnectError("offline in test")

        with patch("httpx.get", side_effect=capture_get):
            updater.check_and_update(registry_url=None)

        assert len(captured_urls) == 1
        assert "huggingface.co" in captured_urls[0] or "manifest.json" in captured_urls[0]

    def test_update_result_is_dataclass(self, tmp_path: Path) -> None:
        """UpdateResult must be a dataclass with expected fields."""
        result = UpdateResult(updated=False, version=None, message="test")
        assert result.updated is False
        assert result.version is None
        assert result.message == "test"
