"""
Unit tests for newly implemented Garmin client features.

Tests for SpO2, Respiration, Hydration, Floors, Intensity Minutes, and HRV data fetching.
"""

import pytest
from datetime import date
from unittest.mock import Mock, patch, MagicMock
from typing import Dict, Any

from health.services.garmin_client import GarminHealthClient
from health.models.daily_metrics import (
    SpO2Data,
    RespirationData,
    HydrationData,
    FloorsData,
    IntensityMinutesData,
    HRVData,
)
from health.utils.exceptions import GarminAuthError, GarminAPIError


@pytest.fixture
def mock_garmin_client() -> GarminHealthClient:
    """Create a mock Garmin client with authentication bypassed."""
    client = GarminHealthClient(email="test@example.com", password="test_password")
    client._authenticated = True
    client.client = Mock()
    return client


class TestFetchSpO2:
    """Tests for fetch_spo2 method."""

    def test_fetch_spo2_success(self, mock_garmin_client: GarminHealthClient) -> None:
        """Test successful SpO2 data fetch."""
        test_date = date(2024, 1, 15)
        mock_response: Dict[str, Any] = {
            "averageSpo2": 96.5,
            "lowestSpo2": 93.0,
            "highestSpo2": 99.0,
            "spo2ValueDescriptorsDTOList": [
                {"timestamp": "2024-01-15T00:00:00", "value": 96.5}
            ],
        }

        mock_garmin_client.client.get_spo2_data = Mock(return_value=mock_response)

        result = mock_garmin_client.fetch_spo2(test_date)

        assert result is not None
        assert isinstance(result, SpO2Data)
        assert result.date == test_date
        assert result.average_spo2 == 96.5
        assert result.min_spo2 == 93.0
        assert result.max_spo2 == 99.0
        assert result.readings is not None
        assert result.raw_data == mock_response

    def test_fetch_spo2_no_data(self, mock_garmin_client: GarminHealthClient) -> None:
        """Test SpO2 fetch when no data is available."""
        test_date = date(2024, 1, 15)
        mock_garmin_client.client.get_spo2_data = Mock(return_value=None)

        result = mock_garmin_client.fetch_spo2(test_date)

        assert result is None

    def test_fetch_spo2_empty_response(
        self, mock_garmin_client: GarminHealthClient
    ) -> None:
        """Test SpO2 fetch with empty response."""
        test_date = date(2024, 1, 15)
        mock_garmin_client.client.get_spo2_data = Mock(return_value={})

        result = mock_garmin_client.fetch_spo2(test_date)

        # Empty dict is falsy, so it should return None
        assert result is None


class TestFetchRespiration:
    """Tests for fetch_respiration method."""

    def test_fetch_respiration_success(
        self, mock_garmin_client: GarminHealthClient
    ) -> None:
        """Test successful respiration data fetch."""
        test_date = date(2024, 1, 15)
        mock_response: Dict[str, Any] = {
            "avgWakingRespirationValue": 14.5,
            "lowestRespirationValue": 12.0,
            "highestRespirationValue": 18.0,
        }

        mock_garmin_client.client.get_respiration_data = Mock(
            return_value=mock_response
        )

        result = mock_garmin_client.fetch_respiration(test_date)

        assert result is not None
        assert isinstance(result, RespirationData)
        assert result.date == test_date
        assert result.average_respiration_rate == 14.5
        assert result.min_respiration_rate == 12.0
        assert result.max_respiration_rate == 18.0
        assert result.raw_data == mock_response

    def test_fetch_respiration_no_data(
        self, mock_garmin_client: GarminHealthClient
    ) -> None:
        """Test respiration fetch when no data is available."""
        test_date = date(2024, 1, 15)
        mock_garmin_client.client.get_respiration_data = Mock(return_value=None)

        result = mock_garmin_client.fetch_respiration(test_date)

        assert result is None


class TestFetchHydration:
    """Tests for fetch_hydration method."""

    def test_fetch_hydration_success(
        self, mock_garmin_client: GarminHealthClient
    ) -> None:
        """Test successful hydration data fetch."""
        test_date = date(2024, 1, 15)
        mock_response: Dict[str, Any] = {
            "valueInML": 2000,
            "sweatLossInML": 2500,
        }

        mock_garmin_client.client.get_hydration_data = Mock(return_value=mock_response)

        result = mock_garmin_client.fetch_hydration(test_date)

        assert result is not None
        assert isinstance(result, HydrationData)
        assert result.date == test_date
        assert result.total_intake_ml == 2000
        assert result.goal_ml == 2500
        assert result.raw_data == mock_response

    def test_fetch_hydration_no_data(
        self, mock_garmin_client: GarminHealthClient
    ) -> None:
        """Test hydration fetch when no data is available."""
        test_date = date(2024, 1, 15)
        mock_garmin_client.client.get_hydration_data = Mock(return_value=None)

        result = mock_garmin_client.fetch_hydration(test_date)

        assert result is None


class TestFetchFloors:
    """Tests for fetch_floors method."""

    def test_fetch_floors_success(self, mock_garmin_client: GarminHealthClient) -> None:
        """Test successful floors data fetch."""
        test_date = date(2024, 1, 15)
        mock_response: Dict[str, Any] = {
            "floorsAscended": 12,
            "floorsDescended": 10,
            "userFloorsAscendedGoal": 15,
        }

        mock_garmin_client.client.get_floors = Mock(return_value=mock_response)

        result = mock_garmin_client.fetch_floors(test_date)

        assert result is not None
        assert isinstance(result, FloorsData)
        assert result.date == test_date
        assert result.floors_climbed == 12
        assert result.floors_descended == 10
        assert result.floor_goal == 15
        assert result.raw_data == mock_response

    def test_fetch_floors_no_data(
        self, mock_garmin_client: GarminHealthClient
    ) -> None:
        """Test floors fetch when no data is available."""
        test_date = date(2024, 1, 15)
        mock_garmin_client.client.get_floors = Mock(return_value=None)

        result = mock_garmin_client.fetch_floors(test_date)

        assert result is None


class TestFetchIntensityMinutes:
    """Tests for fetch_intensity_minutes method."""

    def test_fetch_intensity_minutes_success(
        self, mock_garmin_client: GarminHealthClient
    ) -> None:
        """Test successful intensity minutes data fetch."""
        test_date = date(2024, 1, 15)
        mock_response: Dict[str, Any] = {
            "moderateIntensityMinutes": 20,
            "vigorousIntensityMinutes": 15,
            "intensityMinutesGoal": 150,
            "weeklyGoal": 150,
        }

        mock_garmin_client.client.get_intensity_minutes_data = Mock(
            return_value=mock_response
        )

        result = mock_garmin_client.fetch_intensity_minutes(test_date)

        assert result is not None
        assert isinstance(result, IntensityMinutesData)
        assert result.date == test_date
        assert result.moderate_minutes == 20
        assert result.vigorous_minutes == 15
        assert result.total_minutes == 150
        assert result.weekly_goal == 150
        assert result.raw_data == mock_response

    def test_fetch_intensity_minutes_no_data(
        self, mock_garmin_client: GarminHealthClient
    ) -> None:
        """Test intensity minutes fetch when no data is available."""
        test_date = date(2024, 1, 15)
        mock_garmin_client.client.get_intensity_minutes_data = Mock(return_value=None)

        result = mock_garmin_client.fetch_intensity_minutes(test_date)

        assert result is None


class TestFetchHRV:
    """Tests for fetch_hrv method."""

    def test_fetch_hrv_success(self, mock_garmin_client: GarminHealthClient) -> None:
        """Test successful HRV data fetch."""
        test_date = date(2024, 1, 15)
        mock_response: Dict[str, Any] = {
            "lastNightAvg": 65.5,
            "baseline": {"lowUpper": 70.0},
            "status": "BALANCED",
        }

        mock_garmin_client.client.get_hrv_data = Mock(return_value=mock_response)

        result = mock_garmin_client.fetch_hrv(test_date)

        assert result is not None
        assert isinstance(result, HRVData)
        assert result.date == test_date
        assert result.hrv_value == 65.5
        assert result.baseline_hrv == 70.0
        assert result.status == "BALANCED"
        assert result.raw_data == mock_response

    def test_fetch_hrv_no_baseline(
        self, mock_garmin_client: GarminHealthClient
    ) -> None:
        """Test HRV fetch with no baseline data."""
        test_date = date(2024, 1, 15)
        mock_response: Dict[str, Any] = {
            "lastNightAvg": 65.5,
            "status": "BALANCED",
        }

        mock_garmin_client.client.get_hrv_data = Mock(return_value=mock_response)

        result = mock_garmin_client.fetch_hrv(test_date)

        assert result is not None
        assert result.hrv_value == 65.5
        assert result.baseline_hrv is None
        assert result.status == "BALANCED"

    def test_fetch_hrv_no_data(self, mock_garmin_client: GarminHealthClient) -> None:
        """Test HRV fetch when no data is available."""
        test_date = date(2024, 1, 15)
        mock_garmin_client.client.get_hrv_data = Mock(return_value=None)

        result = mock_garmin_client.fetch_hrv(test_date)

        assert result is None


class TestErrorHandling:
    """Tests for error handling in new methods."""

    def test_unauthenticated_client_spo2(self) -> None:
        """Test that unauthenticated client raises error for SpO2."""
        client = GarminHealthClient(email="test@example.com", password="test_password")
        test_date = date(2024, 1, 15)

        with pytest.raises(GarminAuthError):
            client.fetch_spo2(test_date)

    def test_unauthenticated_client_hrv(self) -> None:
        """Test that unauthenticated client raises error for HRV."""
        client = GarminHealthClient(email="test@example.com", password="test_password")
        test_date = date(2024, 1, 15)

        with pytest.raises(GarminAuthError):
            client.fetch_hrv(test_date)

    def test_api_error_handling_floors(
        self, mock_garmin_client: GarminHealthClient
    ) -> None:
        """Test API error handling for floors data."""
        test_date = date(2024, 1, 15)
        mock_garmin_client.client.get_floors = Mock(
            side_effect=Exception("API Error")
        )

        # _retry_api_call will retry and then raise GarminAPIError
        with pytest.raises(GarminAPIError):
            mock_garmin_client.fetch_floors(test_date)
