"""
Frontend integration tests for existing UI components.

This module tests the integration between the existing JavaScript frontend
(app.js, websocket_client.js, queue.js) and the FastAPI backend, ensuring:
- Authentication flow with JWT tokens works correctly
- WebSocket connections and real-time updates function properly
- API endpoints respond with expected data formats
- Frontend JavaScript can interact with backend services
"""
from unittest.mock import AsyncMock, patch

import pytest
from httpx import ASGITransport, AsyncClient

from src.server.fastapi_app import app
from src.server.services.auth_service import auth_service
from src.server.services.websocket_service import get_websocket_service


@pytest.fixture(autouse=True)
def reset_auth():
    """Reset authentication state before each test."""
    # Store original state
    original_hash = auth_service._hash
    # Reset to unconfigured state
    auth_service._hash = None
    if hasattr(auth_service, '_failed'):
        auth_service._failed.clear()
    yield
    # Restore original state after test
    auth_service._hash = original_hash
    if hasattr(auth_service, '_failed'):
        auth_service._failed.clear()


@pytest.fixture
async def client():
    """Create async test client."""
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac


@pytest.fixture
async def authenticated_client(client):
    """Create authenticated test client with JWT token."""
    # Setup anime directory in settings
    import tempfile

    from src.config.settings import settings
    settings.anime_directory = tempfile.gettempdir()
    
    # Reset series app to pick up new directory
    from src.server.utils.dependencies import reset_series_app
    reset_series_app()
    
    # Setup master password
    await client.post(
        "/api/auth/setup",
        json={"master_password": "StrongP@ss123"}
    )
    
    # Login to get token
    response = await client.post(
        "/api/auth/login",
        json={"password": "StrongP@ss123"}
    )
    
    data = response.json()
    token = data["access_token"]
    
    # Set authorization header for future requests
    client.headers["Authorization"] = f"Bearer {token}"
    
    yield client


class TestFrontendAuthentication:
    """Test authentication flow as used by frontend JavaScript."""
    
    async def test_auth_status_endpoint_not_configured(self, client):
        """Test /api/auth/status when master password not configured."""
        response = await client.get("/api/auth/status")
        
        assert response.status_code == 200
        data = response.json()
        assert data["configured"] is False
        assert data["authenticated"] is False
    
    async def test_auth_status_configured_not_authenticated(self, client):
        """Test /api/auth/status when configured but not authenticated."""
        # Setup master password
        await client.post(
            "/api/auth/setup",
            json={"master_password": "StrongP@ss123"}
        )
        
        response = await client.get("/api/auth/status")
        
        assert response.status_code == 200
        data = response.json()
        assert data["configured"] is True
        assert data["authenticated"] is False
    
    async def test_auth_status_authenticated(self, authenticated_client):
        """Test /api/auth/status when authenticated with JWT."""
        response = await authenticated_client.get("/api/auth/status")
        
        assert response.status_code == 200
        data = response.json()
        assert data["configured"] is True
        assert data["authenticated"] is True
    
    async def test_login_returns_jwt_token(self, client):
        """Test login returns JWT token in format expected by app.js."""
        # Setup
        await client.post(
            "/api/auth/setup",
            json={"master_password": "StrongP@ss123"}
        )
        
        # Login
        response = await client.post(
            "/api/auth/login",
            json={"password": "StrongP@ss123"}
        )
        
        assert response.status_code == 200
        data = response.json()
        assert "access_token" in data
        assert "token_type" in data
        assert data["token_type"] == "bearer"
        assert isinstance(data["access_token"], str)
        assert len(data["access_token"]) > 0
    
    async def test_logout_endpoint(self, authenticated_client):
        """Test logout endpoint clears authentication."""
        response = await authenticated_client.post("/api/auth/logout")
        
        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "ok"
        assert data["message"] == "Logged out successfully"
    
    async def test_unauthorized_request_returns_401(self, client):
        """Test that requests without token return 401."""
        # Setup auth
        await client.post(
            "/api/auth/setup",
            json={"master_password": "StrongP@ss123"}
        )
        
        # Try to access protected endpoint without token
        response = await client.get("/api/anime/")
        
        assert response.status_code == 401
    
    async def test_authenticated_request_succeeds(self, authenticated_client):
        """Test that requests with valid token succeed."""
        mock_anime_service = AsyncMock()
        mock_anime_service.list_series_with_filters = AsyncMock(return_value=[])

        from src.server.utils.dependencies import get_anime_service
        app.dependency_overrides[get_anime_service] = lambda: mock_anime_service

        try:
            response = await authenticated_client.get("/api/anime/")
            assert response.status_code == 200
        finally:
            app.dependency_overrides.pop(get_anime_service, None)


class TestFrontendAnimeAPI:
    """Test anime API endpoints as used by app.js."""
    
    async def test_get_anime_list(self, authenticated_client):
        """Test GET /api/anime returns anime list in expected format."""
        # This test works with the real SeriesApp which scans /tmp
        # Since /tmp has no anime folders, it returns empty list
        response = await authenticated_client.get("/api/anime/")
        
        assert response.status_code == 200
        data = response.json()
        assert isinstance(data, list)
        # The list may be empty if no anime with missing episodes
    
    async def test_search_anime(self, authenticated_client):
        """Test GET /api/anime/search returns search results."""
        # This test actually calls the real aniworld API
        response = await authenticated_client.get(
            "/api/anime/search",
            params={"query": "naruto"}
        )
        
        assert response.status_code == 200
        data = response.json()
        assert isinstance(data, list)
        # Search should return results (actual API call)
        if len(data) > 0:
            assert "name" in data[0]
    
    async def test_rescan_anime(self, authenticated_client):
        """Test POST /api/anime/rescan triggers rescan with events."""
        from unittest.mock import MagicMock, patch

        from src.server.services.progress_service import ProgressService
        from src.server.utils.dependencies import get_anime_service

        # Mock the underlying SeriesApp
        mock_series_app = MagicMock()
        mock_series_app.directory_to_search = "/tmp/test"
        mock_series_app.series_list = []
        mock_series_app.rescan = AsyncMock(return_value=[])
        mock_series_app.download_status = None
        mock_series_app.scan_status = None
        
        # Mock the ProgressService
        mock_progress_service = MagicMock(spec=ProgressService)
        mock_progress_service.start_progress = AsyncMock()
        mock_progress_service.update_progress = AsyncMock()
        mock_progress_service.complete_progress = AsyncMock()
        mock_progress_service.fail_progress = AsyncMock()
        
        # Create real AnimeService with mocked dependencies
        from src.server.services.anime_service import AnimeService
        anime_service = AnimeService(
            series_app=mock_series_app,
            progress_service=mock_progress_service,
        )
        
        # Override the dependency
        app.dependency_overrides[get_anime_service] = lambda: anime_service
        
        try:
            # Mock database operations called during rescan
            with patch.object(
                anime_service, '_save_scan_results_to_db', new_callable=AsyncMock
            ):
                with patch.object(
                    anime_service, '_load_series_from_db', new_callable=AsyncMock
                ):
                    response = await authenticated_client.post(
                        "/api/anime/rescan"
                    )
            
            assert response.status_code == 200
            data = response.json()
            assert data["success"] is True
            # Verify rescan was called on the underlying SeriesApp
            mock_series_app.rescan.assert_called_once()
        finally:
            # Clean up override
            app.dependency_overrides.pop(get_anime_service, None)


class TestFrontendDownloadAPI:
    """Test download API endpoints as used by app.js and queue.js."""
    
    async def test_add_to_download_queue(self, authenticated_client):
        """Test adding episodes to download queue."""
        response = await authenticated_client.post(
            "/api/queue/add",
            json={
                "serie_id": "test-anime",
                "serie_folder": "Test Anime (2024)",
                "serie_name": "Test Anime",
                "episodes": [{"season": 1, "episode": 1}],
                "priority": "normal"
            }
        )
        
        # Should return 201 for successful creation
        assert response.status_code == 201
        data = response.json()
        assert "added_items" in data
    
    async def test_get_queue_status(self, authenticated_client):
        """Test GET /api/queue/status returns queue status."""
        response = await authenticated_client.get("/api/queue/status")
        
        assert response.status_code == 200
        data = response.json()
        # Check for expected response structure (nested status)
        assert "status" in data
        assert "statistics" in data
    
    async def test_start_download_queue(self, authenticated_client):
        """Test POST /api/queue/start starts next download."""
        response = await authenticated_client.post("/api/queue/start")
        
        # Should return 200 with success message, or 400 if queue is empty
        assert response.status_code in [200, 400]
        data = response.json()
        if response.status_code == 200:
            assert "message" in data or "status" in data
    
    async def test_stop_download_queue(self, authenticated_client):
        """Test POST /api/queue/stop stops processing new downloads."""
        response = await authenticated_client.post("/api/queue/stop")
        
        assert response.status_code == 200
        data = response.json()
        assert "message" in data or "status" in data


class TestFrontendWebSocketIntegration:
    """Test WebSocket integration as used by websocket_client.js."""
    
    async def test_websocket_connection(self, authenticated_client):
        """Test WebSocket connection establishment using mock."""
        # Create a mock WebSocket
        mock_ws = AsyncMock()
        mock_ws.accept = AsyncMock()
        
        ws_service = get_websocket_service()
        connection_id = "test-frontend-conn"
        
        # Test connection flow
        await ws_service.manager.connect(mock_ws, connection_id)
        
        # Verify connection was established
        mock_ws.accept.assert_called_once()
        count = await ws_service.manager.get_connection_count()
        assert count >= 1
        
        # Cleanup
        await ws_service.manager.disconnect(connection_id)
    
    async def test_websocket_receives_queue_updates(self, authenticated_client):
        """Test WebSocket receives queue status updates."""
        # Create a mock WebSocket
        mock_ws = AsyncMock()
        mock_ws.accept = AsyncMock()
        mock_ws.send_json = AsyncMock()
        
        ws_service = get_websocket_service()
        connection_id = "test-queue-update"
        
        # Connect the mock WebSocket and join the downloads room
        await ws_service.manager.connect(mock_ws, connection_id)
        await ws_service.manager.join_room(connection_id, "downloads")
        
        # Simulate queue update broadcast using service method
        ws_service = get_websocket_service()
        await ws_service.broadcast_queue_status({
            "action": "items_added",
            "total_items": 1,
            "added_ids": ["item_123"]
        })
        
        # Verify the broadcast was sent
        assert mock_ws.send_json.called
        
        # Cleanup
        await ws_service.manager.disconnect(connection_id)
    
    async def test_websocket_receives_download_progress(
        self, authenticated_client
    ):
        """Test WebSocket receives download progress updates."""
        # Create a mock WebSocket
        mock_ws = AsyncMock()
        mock_ws.accept = AsyncMock()
        mock_ws.send_json = AsyncMock()
        
        ws_service = get_websocket_service()
        connection_id = "test-download-progress"
        
        # Connect the mock WebSocket and join the downloads room
        await ws_service.manager.connect(mock_ws, connection_id)
        await ws_service.manager.join_room(connection_id, "downloads")
        
        # Simulate progress update using service method
        progress_data = {
            "serie_name": "Test Anime",
            "episode": {"season": 1, "episode": 1},
            "progress": 0.5,
            "speed": "2.5 MB/s",
            "eta": "00:02:30"
        }
        
        ws_service = get_websocket_service()
        await ws_service.broadcast_download_progress(
            "item_123", progress_data
        )
        
        # Verify the broadcast was sent
        assert mock_ws.send_json.called
        
        # Cleanup
        await ws_service.manager.disconnect(connection_id)


class TestFrontendConfigAPI:
    """Test configuration API endpoints as used by app.js."""
    
    async def test_get_config(self, authenticated_client):
        """Test GET /api/config returns configuration."""
        response = await authenticated_client.get("/api/config")
        
        assert response.status_code == 200
        data = response.json()
        # Check for actual config fields returned by the API
        assert isinstance(data, dict)
        assert len(data) > 0  # Config should have some fields
    
    async def test_update_config(self, authenticated_client):
        """Test POST /api/config updates configuration."""
        # Check what method is actually supported - might be PUT or PATCH
        response = await authenticated_client.put(
            "/api/config",
            json={"name": "Test Config"}
        )
        
        # Should accept the request or return method not allowed
        assert response.status_code in [200, 400, 405]


class TestFrontendJavaScriptIntegration:
    """Test JavaScript functionality integration with backend."""
    
    async def test_makeAuthenticatedRequest_bearer_token(
        self, authenticated_client
    ):
        """Test frontend's makeAuthenticatedRequest pattern works."""
        # Simulate what app.js does: include Bearer token header
        token = authenticated_client.headers.get(
            "Authorization", ""
        ).replace("Bearer ", "")
        
        response = await authenticated_client.get(
            "/api/anime/",
            headers={"Authorization": f"Bearer {token}"}
        )
        
        # Should work with properly formatted token
        assert response.status_code == 200
    
    async def test_frontend_handles_401_gracefully(self, client):
        """Test that 401 responses can be detected by frontend."""
        # Setup auth
        await client.post(
            "/api/auth/setup",
            json={"master_password": "StrongP@ss123"}
        )
        
        # Try accessing protected endpoint without token
        response = await client.get("/api/anime/")
        
        assert response.status_code == 401
        # Frontend JavaScript checks for 401 and redirects to login
    
    async def test_queue_operations_compatibility(self, authenticated_client):
        """Test queue operations match queue.js expectations."""
        # Test start - should return 400 when queue is empty (valid behavior)
        response = await authenticated_client.post("/api/queue/start")
        assert response.status_code in [200, 400]
        if response.status_code == 400:
            # Verify error message indicates empty queue
            assert "No pending downloads" in response.json()["message"]
        
        # Test pause - always succeeds even if nothing is processing
        response = await authenticated_client.post("/api/queue/pause")
        assert response.status_code == 200
        
        # Test stop - always succeeds even if nothing is processing
        response = await authenticated_client.post("/api/queue/stop")
        assert response.status_code == 200


class TestFrontendErrorHandling:
    """Test error handling as expected by frontend JavaScript."""
    
    async def test_api_error_returns_json(self, authenticated_client):
        """Test that API errors return JSON format expected by frontend."""
        # Test with a non-existent endpoint
        response = await authenticated_client.get(
            "/api/nonexistent"
        )
        
        # Should return error response (404 or other error code)
        assert response.status_code >= 400
    
    async def test_validation_error_returns_400(self, authenticated_client):
        """Test that validation errors return 400/422 with details."""
        # Send invalid data to queue/add endpoint
        response = await authenticated_client.post(
            "/api/queue/add",
            json={}  # Empty request should fail validation
        )
        
        # Should return validation error
        assert response.status_code in [400, 422]


class TestFrontendRealTimeUpdates:
    """Test real-time update scenarios as used by frontend."""
    
    async def test_download_started_notification(self, authenticated_client):
        """Test that download_started events are broadcasted."""
        # Create mock WebSocket
        mock_ws = AsyncMock()
        mock_ws.accept = AsyncMock()
        mock_ws.send_json = AsyncMock()
        
        ws_service = get_websocket_service()
        connection_id = "test-download-started"
        
        # Connect the mock WebSocket
        await ws_service.manager.connect(mock_ws, connection_id)
        
        # Simulate download started broadcast using system message
        await ws_service.broadcast_system_message("download_started", {
            "item_id": "item_123",
            "serie_name": "Test Anime"
        })
        
        # Verify broadcast was sent
        assert mock_ws.send_json.called
        
        # Cleanup
        await ws_service.manager.disconnect(connection_id)
    
    async def test_download_completed_notification(self, authenticated_client):
        """Test that download_completed events are broadcasted."""
        # Create mock WebSocket
        mock_ws = AsyncMock()
        mock_ws.accept = AsyncMock()
        mock_ws.send_json = AsyncMock()
        
        ws_service = get_websocket_service()
        connection_id = "test-download-completed"
        
        # Connect the mock WebSocket and join the downloads room
        await ws_service.manager.connect(mock_ws, connection_id)
        await ws_service.manager.join_room(connection_id, "downloads")
        
        # Simulate download completed broadcast
        await ws_service.broadcast_download_complete("item_123", {
            "serie_name": "Test Anime",
            "episode": {"season": 1, "episode": 1}
        })
        
        # Verify broadcast was sent
        assert mock_ws.send_json.called
        
        # Cleanup
        await ws_service.manager.disconnect(connection_id)
    
    async def test_multiple_clients_receive_broadcasts(
        self, authenticated_client
    ):
        """Test that multiple WebSocket clients receive broadcasts."""
        # Create two mock WebSockets
        mock_ws1 = AsyncMock()
        mock_ws1.accept = AsyncMock()
        mock_ws1.send_json = AsyncMock()
        
        mock_ws2 = AsyncMock()
        mock_ws2.accept = AsyncMock()
        mock_ws2.send_json = AsyncMock()
        
        ws_service = get_websocket_service()
        
        # Connect both mock WebSockets
        await ws_service.manager.connect(mock_ws1, "test-client-1")
        await ws_service.manager.connect(mock_ws2, "test-client-2")
        
        # Broadcast to all using system message
        await ws_service.broadcast_system_message(
            "test_event", {"message": "hello"}
        )
        
        # Both should have received it
        assert mock_ws1.send_json.called
        assert mock_ws2.send_json.called
        
        # Cleanup
        await ws_service.manager.disconnect("test-client-1")
        await ws_service.manager.disconnect("test-client-2")


class TestFrontendDataFormats:
    """Test that backend returns data in formats expected by frontend."""
    
    async def test_anime_list_format(self, authenticated_client):
        """Test anime list has required fields for frontend rendering."""
        # Get the actual anime list from the service (follow redirects)
        response = await authenticated_client.get(
            "/api/anime", follow_redirects=True
        )
        
        # Should return successfully
        assert response.status_code == 200
        data = response.json()
        
        # Should be a list
        assert isinstance(data, list)
        
        # If there are anime, check the structure
        if data:
            anime = data[0]
            # Frontend expects these fields
            assert "name" in anime or "title" in anime
    
    async def test_queue_status_format(self, authenticated_client):
        """Test queue status has required fields for queue.js."""
        # Use the correct endpoint path (follow redirects)
        response = await authenticated_client.get(
            "/api/queue/status", follow_redirects=True
        )
        
        # Should return successfully
        assert response.status_code == 200
        data = response.json()
        
        # Frontend expects these fields for queue status
        assert "items" in data or "queue" in data or "status" in data
        # Status endpoint should return a valid response structure
        assert isinstance(data, dict)
    
    async def test_websocket_message_format(self, authenticated_client):
        """Test WebSocket messages match websocket_client.js expectations."""
        # Create mock WebSocket
        mock_ws = AsyncMock()
        mock_ws.accept = AsyncMock()
        mock_ws.send_json = AsyncMock()
        
        ws_service = get_websocket_service()
        connection_id = "test-message-format"
        
        # Connect the mock WebSocket
        await ws_service.manager.connect(mock_ws, connection_id)
        
        # Broadcast a message
        await ws_service.broadcast_system_message(
            "test_type", {"test_key": "test_value"}
        )
        
        # Verify message was sent with correct format
        assert mock_ws.send_json.called
        call_args = mock_ws.send_json.call_args[0][0]
        
        # WebSocket client expects type and data fields
        assert "type" in call_args
        assert "data" in call_args
        assert isinstance(call_args["data"], dict)
        
        # Cleanup
        await ws_service.manager.disconnect(connection_id)
