"""Integration tests for aiokwikset library.

This module contains integration-style tests that verify the library
works correctly as a whole, testing typical usage patterns and
workflows that end users would follow.
"""

from __future__ import annotations

import asyncio
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from aiokwikset import (
    API,
    InvalidDeviceError,
    MFAChallengeRequired,
    Unauthenticated,
)

from .conftest import (
    MOCK_COMMAND_RESPONSE,
    MOCK_DEVICES,
    MOCK_HOMES,
    MOCK_SRP_SUCCESS_RESPONSE,
    MOCK_USER_INFO,
    MOCK_VALID_DEVICE,
    MOCK_VALID_USER,
    MockCognito,
)

# ============================================================================
# Full Authentication Flow Tests
# ============================================================================


class TestAuthenticationFlow:
    """Tests for complete authentication workflows."""

    @pytest.mark.asyncio
    async def test_basic_login_and_use(self) -> None:
        """Test basic login and API usage flow."""
        api = API()

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_cognito = MockCognito()
            mock_create.return_value = mock_cognito
            mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE

            # Login
            await api.async_login("user@example.com", "password123")

        # Verify authenticated state
        assert api.is_authenticated
        assert api.device is not None
        assert api.user is not None

        # Clean up
        await api.async_close()
        assert not api.is_authenticated

    @pytest.mark.asyncio
    async def test_login_with_mfa_flow(self) -> None:
        """Test complete MFA login flow."""
        api = API()

        # Mock the MFA challenge cognito (used after MFA response)
        mock_cognito = MockCognito(
            id_token="id_after_mfa",
            access_token="access_after_mfa",
            refresh_token="refresh_after_mfa",
        )

        from pycognito.exceptions import SoftwareTokenMFAChallengeException

        exc = SoftwareTokenMFAChallengeException.__new__(SoftwareTokenMFAChallengeException)
        exc._tokens = {"session": "mfa_session"}

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = mock_cognito
            mock_srp.side_effect = exc

            # First login attempt raises MFA challenge
            with pytest.raises(MFAChallengeRequired) as exc_info:
                await api.async_login("user@example.com", "password123")

            # Respond to MFA challenge
            await api.async_respond_to_mfa_challenge(
                mfa_code="123456",
                mfa_type=exc_info.value.mfa_type,
                mfa_tokens=exc_info.value.mfa_tokens,
            )

        assert api.is_authenticated

    @pytest.mark.asyncio
    async def test_context_manager_flow(self) -> None:
        """Test using API as async context manager."""
        async with API() as api:
            with (
                patch.object(api, "_create_cognito_client") as mock_create,
                patch.object(api, "_authenticate_srp") as mock_srp,
            ):
                mock_create.return_value = MockCognito()
                mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE
                await api.async_login("user@example.com", "password123")

                assert api.is_authenticated

        # After context, should be cleaned up
        assert not api.is_authenticated


# ============================================================================
# Device Operation Flow Tests
# ============================================================================


class TestDeviceOperationFlow:
    """Tests for device operation workflows."""

    @pytest.mark.asyncio
    async def test_get_devices_and_lock(self) -> None:
        """Test getting devices and locking one."""
        api = API()

        # Setup mocks
        async def mock_request(
            _method: str,
            url: str,
            **_kwargs: Any,
        ) -> dict[str, Any]:
            if "homes" in url and "devices" in url:
                return MOCK_DEVICES
            elif "status" in url:
                return MOCK_COMMAND_RESPONSE
            return {}

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = MockCognito()
            mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE
            await api.async_login("user@example.com", "password123")

        # Replace request method on the Device instance
        api.device._request = AsyncMock(side_effect=mock_request)  # type: ignore

        # Get devices
        devices = await api.device.get_devices("home-001")
        assert len(devices) == 2

        # Lock first device
        device_to_lock = devices[0]
        result = await api.device.lock_device(device_to_lock, MOCK_VALID_USER)

        assert result.success

    @pytest.mark.asyncio
    async def test_accessory_settings_flow(self) -> None:
        """Test changing multiple accessory settings."""
        api = API()

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = MockCognito()
            mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE
            await api.async_login("user@example.com", "password123")

        # Mock requests on Device instance
        api.device._request = AsyncMock(return_value=MOCK_COMMAND_RESPONSE)  # type: ignore

        # Enable LED
        result = await api.device.set_led_enabled(MOCK_VALID_DEVICE, True)
        assert result.success

        # Disable audio
        result = await api.device.set_audio_enabled(MOCK_VALID_DEVICE, False)
        assert result.success

        # Enable secure screen
        result = await api.device.set_secure_screen_enabled(MOCK_VALID_DEVICE, True)
        assert result.success


# ============================================================================
# User Information Flow Tests
# ============================================================================


class TestUserInformationFlow:
    """Tests for user information workflows."""

    @pytest.mark.asyncio
    async def test_get_user_and_homes(self) -> None:
        """Test getting user info and listing homes."""
        api = API()

        async def mock_request(
            _method: str,
            url: str,
            **_kwargs: Any,
        ) -> dict[str, Any]:
            if "homes" in url:
                return MOCK_HOMES
            elif "users/me" in url:
                return MOCK_USER_INFO
            return {}

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = MockCognito()
            mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE
            await api.async_login("user@example.com", "password123")

        # Mock request on User instance
        api.user._request = AsyncMock(side_effect=mock_request)  # type: ignore

        # Get user info
        user_info = await api.user.get_info()
        assert user_info is not None
        assert user_info["email"] == "test@example.com"

        # Get homes
        homes = await api.user.get_homes()
        assert len(homes) == 2
        assert homes[0]["homename"] == "My Home"


# ============================================================================
# Error Handling Flow Tests
# ============================================================================


class TestErrorHandlingFlow:
    """Tests for error handling in typical workflows."""

    @pytest.mark.asyncio
    async def test_invalid_credentials_handling(self) -> None:
        """Test handling invalid credentials during login."""
        api = API()

        from botocore.exceptions import ClientError

        error = ClientError(
            error_response={
                "Error": {
                    "Code": "NotAuthorizedException",
                    "Message": "Incorrect username or password",
                }
            },
            operation_name="InitiateAuth",
        )

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = MockCognito()
            mock_srp.side_effect = error

            with pytest.raises(Unauthenticated):
                await api.async_login("user@example.com", "wrong_password")

        assert not api.is_authenticated

    @pytest.mark.asyncio
    async def test_device_validation_error_handling(self) -> None:
        """Test handling device validation errors."""
        api = API()

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = MockCognito()
            mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE
            await api.async_login("user@example.com", "password123")

        # Mock request on Device instance
        api.device._request = AsyncMock(return_value=MOCK_COMMAND_RESPONSE)  # type: ignore

        # Try to lock with invalid device
        with pytest.raises(InvalidDeviceError) as exc_info:
            await api.device.lock_device(
                {"devicename": "Invalid"},  # Missing serialnumber
                MOCK_VALID_USER,
            )

        assert "serialnumber" in exc_info.value.missing_fields

    @pytest.mark.asyncio
    async def test_request_failure_recovery(self) -> None:
        """Test recovering from request failures."""
        api = API()

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = MockCognito()
            mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE
            await api.async_login("user@example.com", "password123")

        call_count = 0

        async def mock_request_with_retry(
            _method: str,
            _url: str,
            **_kwargs: Any,
        ) -> dict[str, Any]:
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                raise Exception("Temporary failure")
            return MOCK_COMMAND_RESPONSE

        # Mock request on Device instance
        api.device._request = AsyncMock(side_effect=mock_request_with_retry)  # type: ignore

        # First call fails
        result = await api.device.lock_device(MOCK_VALID_DEVICE, MOCK_VALID_USER)
        assert not result.success  # Returns CommandResult with success=False

        # Second call succeeds
        result = await api.device.lock_device(MOCK_VALID_DEVICE, MOCK_VALID_USER)
        assert result.success


# ============================================================================
# Token Refresh Flow Tests
# ============================================================================


class TestTokenRefreshFlow:
    """Tests for token refresh workflows."""

    @pytest.mark.asyncio
    async def test_token_callback_on_refresh(self) -> None:
        """Test token update callback is called on refresh."""
        tokens_received: dict[str, str] = {}

        async def token_callback(id_token: str, access_token: str, refresh_token: str) -> None:
            tokens_received["id"] = id_token
            tokens_received["access"] = access_token
            tokens_received["refresh"] = refresh_token

        api = API(token_update_callback=token_callback)

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = MockCognito()
            mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE
            await api.async_login("user@example.com", "password123")

        assert "id" in tokens_received
        assert "access" in tokens_received
        assert "refresh" in tokens_received

    @pytest.mark.asyncio
    async def test_automatic_token_refresh(self) -> None:
        """Test tokens are automatically checked before requests."""
        # Create a mock session
        mock_response = AsyncMock()
        mock_response.json = AsyncMock(return_value={"data": []})
        mock_response.raise_for_status = MagicMock()

        mock_cm = AsyncMock()
        mock_cm.__aenter__.return_value = mock_response
        mock_cm.__aexit__.return_value = None

        mock_session = MagicMock()
        mock_session.request.return_value = mock_cm
        mock_session.close = AsyncMock()

        # Create API with the mock session
        api = API(websession=mock_session)

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = MockCognito()
            mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE
            await api.async_login("user@example.com", "password123")

        # Track if check_token was called
        check_token_called = False

        async def mock_check_token():
            nonlocal check_token_called
            check_token_called = True

        api.async_check_token = mock_check_token  # type: ignore

        # Make a request - this should call check_token
        await api._request("get", "https://api.example.com/test")

        assert check_token_called


# ============================================================================
# Concurrent Operations Tests
# ============================================================================


class TestConcurrentOperations:
    """Tests for concurrent API operations."""

    @pytest.mark.asyncio
    async def test_concurrent_device_commands(self) -> None:
        """Test multiple concurrent device commands."""
        api = API()

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = MockCognito()
            mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE
            await api.async_login("user@example.com", "password123")

        # Mock request on Device instance
        api.device._request = AsyncMock(return_value=MOCK_COMMAND_RESPONSE)  # type: ignore

        # Run multiple commands concurrently
        devices = [
            {"serialnumber": "SN001", "devicename": "Door 1"},
            {"serialnumber": "SN002", "devicename": "Door 2"},
            {"serialnumber": "SN003", "devicename": "Door 3"},
        ]

        results = await asyncio.gather(
            *[api.device.lock_device(d, MOCK_VALID_USER) for d in devices]
        )

        assert len(results) == 3
        assert all(r.success for r in results)

    @pytest.mark.asyncio
    async def test_concurrent_get_operations(self) -> None:
        """Test concurrent get operations."""
        api = API()

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = MockCognito()
            mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE
            await api.async_login("user@example.com", "password123")

        async def mock_device_request(
            _method: str,
            _url: str,
            **_kwargs: Any,
        ) -> dict[str, Any]:
            return MOCK_DEVICES

        async def mock_user_request(
            _method: str,
            url: str,
            **_kwargs: Any,
        ) -> dict[str, Any]:
            if "homes" in url:
                return MOCK_HOMES
            elif "users/me" in url:
                return MOCK_USER_INFO
            return {}

        # Mock requests on the specific instances
        api.device._request = AsyncMock(side_effect=mock_device_request)  # type: ignore
        api.user._request = AsyncMock(side_effect=mock_user_request)  # type: ignore

        # Run get operations concurrently
        results = await asyncio.gather(
            api.user.get_info(),
            api.user.get_homes(),
            api.device.get_devices("home-001"),
        )

        user_info, homes, devices = results
        assert user_info is not None
        assert len(homes) == 2
        assert len(devices) == 2


# ============================================================================
# Edge Case Tests
# ============================================================================


class TestEdgeCases:
    """Tests for edge cases and boundary conditions."""

    @pytest.mark.asyncio
    async def test_empty_response_handling(self) -> None:
        """Test handling of empty API responses."""
        api = API()

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = MockCognito()
            mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE
            await api.async_login("user@example.com", "password123")

        # Mock request on User instance
        api.user._request = AsyncMock(return_value={"data": []})  # type: ignore

        # Empty homes list
        homes = await api.user.get_homes()
        assert homes == []

        # Empty user info
        user_info = await api.user.get_info()
        assert user_info is None

    @pytest.mark.asyncio
    async def test_long_username_handling(self) -> None:
        """Test handling of long usernames in lock commands."""
        api = API()

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = MockCognito()
            mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE
            await api.async_login("user@example.com", "password123")

        # Mock request on Device instance
        api.device._request = AsyncMock(return_value=MOCK_COMMAND_RESPONSE)  # type: ignore

        long_user = {
            "firstname": "VeryLongFirstName",
            "lastname": "VeryLongLastName",
        }

        result = await api.device.lock_device(MOCK_VALID_DEVICE, long_user)

        # Should succeed despite long name (truncated internally)
        assert result.success

    @pytest.mark.asyncio
    async def test_special_characters_in_device_id(self) -> None:
        """Test device operations with special characters in ID."""
        api = API()

        with (
            patch.object(api, "_create_cognito_client") as mock_create,
            patch.object(api, "_authenticate_srp") as mock_srp,
        ):
            mock_create.return_value = MockCognito()
            mock_srp.return_value = MOCK_SRP_SUCCESS_RESPONSE
            await api.async_login("user@example.com", "password123")

        # Mock request on Device instance
        api.device._request = AsyncMock(return_value=MOCK_COMMAND_RESPONSE)  # type: ignore

        device = {
            "serialnumber": "SN-2024/ABC_123",
            "devicename": "Test Device",
        }

        result = await api.device.lock_device(device, MOCK_VALID_USER)
        assert result.success
        assert result.device_id == "SN-2024/ABC_123"
