#!/usr/bin/env python3
"""
Test Suite: Resource Cleanup (UVS-H35)
======================================
Tests for proper cleanup of processes and connections.

VERIFICATION_STAMP
Story: UVS-H35
Verified By: Claude Opus 4.5
Verified At: 2026-02-03
"""

import sys
import asyncio
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
import gc

sys.path.insert(0, '/mnt/e/genesis-system')

from core.browser_controller import (
    BrowserController,
    BrowserConfig,
    PlaywrightBackend,
    HTTPClientBackend,
    ArchiveBackend
)


class TestBrowserControllerCleanup:
    """Tests for BrowserController resource cleanup."""

    @pytest.mark.asyncio
    async def test_close_calls_all_backends(self):
        """Close method calls close on all backends."""
        controller = BrowserController()

        close_calls = []
        for backend in controller._backends:
            original_close = backend.close

            async def tracked_close(name=backend.name()):
                close_calls.append(name)

            backend.close = tracked_close

        await controller.close()

        # All backends should have been closed
        assert len(close_calls) == len(controller._backends)

    @pytest.mark.asyncio
    async def test_close_handles_backend_errors(self):
        """Close continues even if a backend fails."""
        controller = BrowserController()

        async def failing_close():
            raise RuntimeError("Close failed")

        # Make first backend fail
        controller._backends[0].close = failing_close

        # Should not raise
        await controller.close()

    @pytest.mark.asyncio
    async def test_tracked_tasks_cancelled_on_close(self):
        """All tracked tasks are cancelled during close."""
        controller = BrowserController()
        controller._tracked_tasks = set()

        # Create some fake tasks
        async def dummy_task():
            await asyncio.sleep(100)

        task1 = asyncio.create_task(dummy_task())
        task2 = asyncio.create_task(dummy_task())
        controller._tracked_tasks.add(task1)
        controller._tracked_tasks.add(task2)

        await controller.close()

        assert task1.cancelled() or task1.done()
        assert task2.cancelled() or task2.done()
        assert len(controller._tracked_tasks) == 0


class TestPlaywrightBackendCleanup:
    """Tests for Playwright backend cleanup."""

    @pytest.mark.asyncio
    async def test_close_stops_browser(self):
        """Close properly stops browser instance."""
        backend = PlaywrightBackend()

        # Mock browser
        mock_browser = MagicMock()
        mock_browser.close = AsyncMock()
        backend._browser = mock_browser

        mock_playwright = MagicMock()
        mock_playwright.stop = AsyncMock()
        backend._playwright = mock_playwright

        await backend.close()

        mock_browser.close.assert_called_once()
        mock_playwright.stop.assert_called_once()

    @pytest.mark.asyncio
    async def test_close_without_init_safe(self):
        """Close when not initialized doesn't error."""
        backend = PlaywrightBackend()

        # Should not raise
        await backend.close()


class TestHTTPClientCleanup:
    """Tests for HTTP client cleanup."""

    @pytest.mark.asyncio
    async def test_close_closes_session(self):
        """Close properly closes HTTP session."""
        backend = HTTPClientBackend()

        # Mock session
        mock_session = MagicMock()
        mock_session.aclose = AsyncMock()
        backend._session = mock_session

        await backend.close()

        mock_session.aclose.assert_called_once()

    @pytest.mark.asyncio
    async def test_close_without_session_safe(self):
        """Close when no session doesn't error."""
        backend = HTTPClientBackend()
        backend._session = None

        # Should not raise
        await backend.close()


class TestArchiveBackendCleanup:
    """Tests for Archive backend cleanup."""

    @pytest.mark.asyncio
    async def test_close_closes_client(self):
        """Close properly closes HTTP client."""
        backend = ArchiveBackend()

        mock_client = MagicMock()
        mock_client.aclose = AsyncMock()
        backend._client = mock_client

        await backend.close()

        mock_client.aclose.assert_called_once()


class TestContextManagerCleanup:
    """Tests for context manager cleanup (UVS-H22)."""

    @pytest.mark.asyncio
    async def test_context_manager_cleanup_on_success(self):
        """Context manager cleans up after successful use."""
        from core.browser_controller import browser_session

        close_called = False

        with patch.object(BrowserController, 'close') as mock_close:
            mock_close.return_value = asyncio.coroutine(lambda: None)()

            with patch.object(BrowserController, 'initialize') as mock_init:
                mock_init.return_value = asyncio.coroutine(lambda: True)()

                async with browser_session() as controller:
                    pass  # Use the controller

                # Close should have been called
                mock_close.assert_called()

    @pytest.mark.asyncio
    async def test_context_manager_cleanup_on_exception(self):
        """Context manager cleans up even after exception."""
        from core.browser_controller import browser_session

        with patch.object(BrowserController, 'close') as mock_close:
            mock_close.return_value = asyncio.coroutine(lambda: None)()

            with patch.object(BrowserController, 'initialize') as mock_init:
                mock_init.return_value = asyncio.coroutine(lambda: True)()

                try:
                    async with browser_session() as controller:
                        raise RuntimeError("Test error")
                except RuntimeError:
                    pass

                # Close should still have been called
                mock_close.assert_called()


class TestMemoryCleanup:
    """Tests for memory cleanup."""

    def test_history_bounded(self):
        """History uses bounded deque."""
        controller = BrowserController()

        # _history should be a deque with maxlen
        from collections import deque
        assert isinstance(controller._history, deque)
        assert controller._history.maxlen == 100

    def test_history_doesnt_grow_unbounded(self):
        """Adding to history doesn't grow beyond limit."""
        controller = BrowserController()

        # Add more than maxlen entries
        for i in range(200):
            controller._history.append({'url': f'https://test{i}.com'})

        assert len(controller._history) <= 100

    def test_csrf_tokens_can_be_cleared(self):
        """CSRF token cache can be cleared."""
        controller = BrowserController()

        controller._csrf_tokens['example.com'] = 'token1'
        controller._csrf_tokens['test.com'] = 'token2'

        controller.clear_csrf_tokens()

        assert len(controller._csrf_tokens) == 0


class TestVisionWorkerCleanup:
    """Tests for VisionWorker cleanup."""

    @pytest.mark.asyncio
    async def test_stop_clears_pending_frames(self):
        """Stop clears frame buffer."""
        from core.vision_worker import VisionWorker

        class MockController:
            _initialized = False

        worker = VisionWorker(MockController())
        worker._pending_frames.append(b'frame1')
        worker._pending_frames.append(b'frame2')

        await worker.stop()

        assert len(worker._pending_frames) == 0

    @pytest.mark.asyncio
    async def test_stop_resets_task(self):
        """Stop sets task to None."""
        from core.vision_worker import VisionWorker

        class MockController:
            _initialized = False

        worker = VisionWorker(MockController())

        await worker.stop()

        assert worker._task is None


if __name__ == '__main__':
    pytest.main([__file__, '-v', '--tb=short'])
