"""
Tests for async HTTP client functionality in EarthCatalog.

These tests validate the concurrent HTTP processing capabilities including
performance improvements, error handling, and integration with the existing
pipeline architecture.
"""

# mypy: ignore-errors

import asyncio
import time
from unittest.mock import patch

import pytest

# Import async HTTP testing utilities
try:
    import aiohttp
    from aioresponses import aioresponses

    HAS_ASYNC_TEST_SUPPORT = True
except ImportError:
    HAS_ASYNC_TEST_SUPPORT = False
    aioresponses = None  # type: ignore
    aiohttp = None  # type: ignore

# Import grid systems for validation tests
from earthcatalog.grid_systems import (
    H3GridSystem,
    MGRSGridSystem,
    S2GridSystem,
    SimpleLatLonGrid,
)

# Import EarthCatalog modules
from earthcatalog.ingestion_pipeline import ProcessingConfig, STACIngestionPipeline

# Grid systems are now managed by the pipeline internally

try:
    from earthcatalog.async_http_client import (
        HAS_ASYNC_HTTP,
        AsyncHTTPClient,
        BatchDownloader,
        ErrorType,
        RequestResult,
        download_stac_items_async,
    )
except ImportError:
    HAS_ASYNC_HTTP = False
    # Type stubs for mypy
    AsyncHTTPClient = None  # type: ignore
    BatchDownloader = None  # type: ignore
    RequestResult = None  # type: ignore
    ErrorType = None  # type: ignore
    download_stac_items_async = None  # type: ignore


@pytest.mark.skipif(not HAS_ASYNC_HTTP, reason="Async HTTP client not available")
class TestAsyncHTTPClient:
    """Test suite for AsyncHTTPClient functionality."""

    def setup_method(self):
        """Set up test fixtures."""
        self.test_config = {
            "concurrent_requests": 10,
            "connection_pool_size": 20,
            "request_timeout": 5,
            "retry_attempts": 2,
            "retry_delay": 0.1,
        }

        # Sample STAC item for responses
        self.sample_stac_item = {
            "id": "test_item",
            "type": "Feature",
            "geometry": {"type": "Point", "coordinates": [-122.4, 37.8]},
            "properties": {"datetime": "2024-01-01T00:00:00Z", "collection": "test_collection"},
        }

    @pytest.mark.asyncio
    async def test_async_http_client_initialization(self):
        """Test AsyncHTTPClient can be initialized with proper configuration."""
        async with AsyncHTTPClient(**self.test_config) as client:
            assert client.concurrent_requests == 10
            assert client.connection_pool_size == 20
            assert client.request_timeout == 5
            assert client.retry_attempts == 2
            assert client.retry_delay == 0.1
            assert client.session is not None

    @pytest.mark.skipif(not HAS_ASYNC_TEST_SUPPORT, reason="aioresponses not available")
    @pytest.mark.asyncio
    async def test_single_successful_request(self):
        """Test successful download of a single STAC item."""
        url = "https://example.com/item.json"

        with aioresponses() as mock_responses:
            mock_responses.get(url, payload=self.sample_stac_item)

            async with AsyncHTTPClient(**self.test_config) as client:
                result = await client._fetch_single_url(url)

                assert result.success is True
                assert result.data == self.sample_stac_item
                assert result.error is None
                assert result.attempts == 1
                assert result.response_time > 0

    @pytest.mark.skipif(not HAS_ASYNC_TEST_SUPPORT, reason="aioresponses not available")
    @pytest.mark.asyncio
    async def test_batch_download(self):
        """Test concurrent download of multiple STAC items."""
        urls = [f"https://example.com/item_{i}.json" for i in range(100)]

        with aioresponses() as mock_responses:
            # Mock all URLs with successful responses
            for i, url in enumerate(urls):
                item = self.sample_stac_item.copy()
                item["id"] = f"item_{i}"
                mock_responses.get(url, payload=item)

            async with AsyncHTTPClient(**self.test_config) as client:
                start_time = time.time()
                results = await client.download_batch(urls)
                duration = time.time() - start_time

                # Validate results
                assert len(results) == 100
                successful = [r for r in results if r.success]
                assert len(successful) == 100

                # Should complete much faster than sequential (under 2 seconds)
                assert duration < 2.0

                # Validate data integrity
                for i, result in enumerate(successful):
                    assert result.data is not None
                    assert result.data["id"] == f"item_{i}"

    @pytest.mark.skipif(not HAS_ASYNC_TEST_SUPPORT, reason="aioresponses not available")
    @pytest.mark.asyncio
    async def test_error_handling_and_retries(self):
        """Test error handling with different HTTP error conditions using mocked responses."""
        # Use mocked responses for fast, reliable testing
        urls = [
            "https://example.com/server_error.json",  # Will mock 500 error
            "https://example.com/not_found.json",  # Will mock 404 error
            "https://example.com/rate_limit.json",  # Will mock 429 error
            "https://example.com/success.json",  # Will mock success
            "https://example.com/timeout.json",  # Will mock timeout
        ]

        # Fast test configuration
        test_config = {
            "concurrent_requests": 5,
            "connection_pool_size": 10,
            "request_timeout": 1,  # Short timeout for fast tests
            "retry_attempts": 2,  # Limited retries for speed
            "retry_delay": 0.01,  # Minimal delay for speed
        }

        with aioresponses() as mock_responses:
            # Mock different error conditions
            mock_responses.get(urls[0], status=500)  # Server error
            mock_responses.get(urls[1], status=404)  # Not found
            mock_responses.get(urls[2], status=429)  # Rate limit
            mock_responses.get(urls[3], payload={"id": "success", "type": "Feature"})  # Success
            # urls[4] not mocked - will timeout quickly

            async with AsyncHTTPClient(**test_config) as client:
                results = await client.download_batch(urls)

                # Validate we got results for all URLs
                assert len(results) == len(urls)

                # Analyze results
                success_count = sum(1 for r in results if r.success)
                failure_count = sum(1 for r in results if not r.success)

                # Should have exactly one success (the mocked success)
                assert success_count >= 1, "Should have at least one successful request"
                assert failure_count >= 1, "Should have failed requests"

                # Check that different error types are detected
                error_types = {r.error_type for r in results if not r.success and r.error_type}
                assert len(error_types) >= 1, "Should detect different error types"

                # Validate retry behavior - failed requests should have retry attempts
                retry_counts = [r.attempts for r in results if not r.success]
                if retry_counts:  # Only check if there are failed requests
                    max_retries = max(retry_counts)
                    assert max_retries >= 1, "Should have retry attempts for failed requests"


@pytest.mark.skipif(not HAS_ASYNC_HTTP, reason="Async HTTP client not available")
class TestBatchDownloader:
    """Test suite for BatchDownloader functionality."""

    def setup_method(self):
        """Set up test fixtures."""
        self.downloader = BatchDownloader(batch_size=50, concurrent_requests=10, request_timeout=5, retry_attempts=2)

    @pytest.mark.skipif(not HAS_ASYNC_TEST_SUPPORT, reason="aioresponses not available")
    @pytest.mark.asyncio
    async def test_batch_processing_with_memory_management(self):
        """Test batch processing handles memory efficiently."""
        # Create 1000 URLs to test batch processing
        urls = [f"https://example.com/item_{i}.json" for i in range(1000)]

        with aioresponses() as mock_responses:
            # Mock all URLs with successful responses
            for i, url in enumerate(urls):
                item = {
                    "id": f"item_{i}",
                    "type": "Feature",
                    "geometry": {"type": "Point", "coordinates": [0, 0]},
                    "properties": {"datetime": "2024-01-01T00:00:00Z"},
                }
                mock_responses.get(url, payload=item)

            # Download all items
            items = await self.downloader.download_all(urls)

            # Validate results
            assert len(items) == 1000
            for i, item in enumerate(items):
                assert item["id"] == f"item_{i}"


class TestAsyncHTTPIntegration:
    """Integration tests for async HTTP with EarthCatalog pipeline."""

    def setup_method(self):
        """Set up test fixtures."""
        import tempfile

        import pandas as pd

        # Create temporary input file
        self.temp_input_file = tempfile.NamedTemporaryFile(suffix=".parquet", delete=False)
        df = pd.DataFrame({"url": ["https://example.com/test.json"]})
        df.to_parquet(self.temp_input_file.name)

        self.config = ProcessingConfig(
            input_file=self.temp_input_file.name,
            output_catalog="/tmp/test_catalog",
            scratch_location="/tmp/test_scratch",
            grid_system="h3",
            grid_resolution=2,
            temporal_bin="month",
            # Enable async HTTP
            enable_concurrent_http=True,
            concurrent_requests=5,
            batch_size=10,
        )

        # Create a local processor for testing
        from earthcatalog.ingestion_pipeline import LocalProcessor

        self.processor = LocalProcessor()
        self.pipeline = STACIngestionPipeline(self.config, self.processor)

    def teardown_method(self):
        """Clean up test fixtures."""
        import os

        if hasattr(self, "temp_input_file") and os.path.exists(self.temp_input_file.name):
            os.unlink(self.temp_input_file.name)
        if hasattr(self, "processor"):
            self.processor.close()

    def test_configuration_validation(self):
        """Test async HTTP configuration validation."""
        # Valid configuration should pass
        self.config.validate()

        # Invalid concurrent_requests should fail
        invalid_config = ProcessingConfig(
            input_file=self.temp_input_file.name,  # Use existing temp file
            output_catalog="/tmp/test_catalog",
            scratch_location="/tmp/test_scratch",
            concurrent_requests=0,
        )

        with pytest.raises(ValueError, match="concurrent_requests must be positive"):
            invalid_config.validate()

    def test_async_fallback_behavior(self):
        """Test graceful fallback when async HTTP is not available."""
        # Create config with async enabled
        config = self.config
        config.enable_concurrent_http = True

        # Mock HAS_ASYNC_HTTP to False to test fallback
        with patch("earthcatalog.ingestion_pipeline.HAS_ASYNC_HTTP", False):
            # Should still create pipeline without errors
            pipeline = STACIngestionPipeline(config, self.processor)

            # Should be able to process small batch (will use sync processing)
            urls = ["https://example.com/item1.json", "https://example.com/item2.json"]

            with patch.object(pipeline, "_download_stac_item") as mock_download:
                mock_download.return_value = {
                    "id": "test_item",
                    "type": "Feature",
                    "geometry": {"type": "Point", "coordinates": [0, 0]},
                    "properties": {"datetime": "2024-01-01T00:00:00Z"},
                }

                # This should use sync processing
                items = pipeline._download_stac_items_batch_async(urls, 1)
                assert len(items) == 2
                assert mock_download.call_count == 2

    @pytest.mark.skipif(not HAS_ASYNC_HTTP, reason="Async HTTP client not available")
    def test_async_processing_configuration(self):
        """Test that pipeline correctly configures async vs sync processing."""
        # Test that async is enabled in the configuration
        assert self.pipeline.config.enable_concurrent_http is True
        assert self.pipeline.config.concurrent_requests == 5  # As configured in setup_method
        assert self.pipeline.config.batch_size == 10  # As configured in setup_method

        # Test that HAS_ASYNC_HTTP is properly detected
        from earthcatalog.async_http_client import HAS_ASYNC_HTTP

        if HAS_ASYNC_HTTP:
            # Test async configuration is valid
            assert self.pipeline.config.concurrent_requests > 0
            assert self.pipeline.config.request_timeout > 0
            assert self.pipeline.config.retry_attempts > 0
        else:
            # If async not available, should still work
            assert self.pipeline.config.enable_concurrent_http is True  # Config can be True even if not available

    @pytest.mark.skipif(not HAS_ASYNC_TEST_SUPPORT, reason="aioresponses not available")
    @pytest.mark.asyncio
    async def test_performance_improvement_validation(self):
        """Test that async processing provides performance improvement."""
        # Create test URLs
        test_urls = [f"https://example.com/item_{i}.json" for i in range(100)]

        sample_item = {
            "id": "test_item",
            "type": "Feature",
            "geometry": {"type": "Point", "coordinates": [0, 0]},
            "properties": {"datetime": "2024-01-01T00:00:00Z"},
        }

        with aioresponses() as mock_responses:
            # Mock all URLs with small delay to simulate network
            for url in test_urls:
                mock_responses.get(url, payload=sample_item)

            # Test async performance
            start_time = time.time()
            async_items = await download_stac_items_async(urls=test_urls, concurrent_requests=20, batch_size=50)
            async_duration = time.time() - start_time

            # Validate async results
            assert len(async_items) == 100
            assert async_duration < 5.0  # Should complete quickly

    def test_error_logging_compatibility(self):
        """Test that async errors are logged in the same format as sync errors."""
        with patch("earthcatalog.ingestion_pipeline.logger"):
            # Create a temporary config with fast timeout for failed requests
            import tempfile

            import pandas as pd

            # Create temp file for fast test
            with tempfile.NamedTemporaryFile(suffix=".parquet", delete=False) as tmp_file:
                df = pd.DataFrame({"url": ["https://invalid-url-fast-fail.example.com/item.json"]})
                df.to_parquet(tmp_file.name)

                # Create config with very short timeouts to fail fast
                fast_config = ProcessingConfig(
                    input_file=tmp_file.name,
                    output_catalog="/tmp/test_catalog",
                    scratch_location="/tmp/test_scratch",
                    enable_concurrent_http=True,
                    concurrent_requests=1,
                    request_timeout=1,  # Very short timeout for fast failure
                    retry_attempts=1,  # Minimal retries for speed
                    retry_delay=0.01,  # Minimal delay
                )

                from earthcatalog.ingestion_pipeline import LocalProcessor, STACIngestionPipeline

                processor = LocalProcessor()
                pipeline = STACIngestionPipeline(fast_config, processor)

                # Test with URLs that will fail quickly
                failed_urls = ["https://invalid-url-fast-fail.example.com/item.json"]

                # This should fail quickly and generate logs
                items = pipeline._download_stac_items_batch_async(failed_urls, 1)

                # Verify error handling occurred (items should be empty or contain errors)
                assert isinstance(items, list)  # Should return a list even on failure

                # Clean up
                import os

                os.unlink(tmp_file.name)
                processor.close()


@pytest.mark.skipif(not HAS_ASYNC_HTTP, reason="Async HTTP client not available")
class TestAsyncHTTPClientEdgeCases:
    """Test edge cases and error conditions for async HTTP client."""

    @pytest.mark.asyncio
    async def test_client_without_context_manager(self):
        """Test that client properly fails when used without context manager."""
        client = AsyncHTTPClient()

        with pytest.raises(RuntimeError, match="not initialized"):
            await client._fetch_single_url("https://example.com/test.json")

    @pytest.mark.asyncio
    async def test_empty_url_list(self):
        """Test handling of empty URL list."""
        async with AsyncHTTPClient() as client:
            results = await client.download_batch([])
            assert results == []

    @pytest.mark.asyncio
    async def test_malformed_urls(self):
        """Test handling of malformed URLs."""
        malformed_urls = ["not-a-url", "ftp://invalid-protocol.com", ""]

        async with AsyncHTTPClient(retry_attempts=1) as client:
            results = await client.download_batch(malformed_urls)

            # All should fail with connection errors
            assert len(results) == 3
            for result in results:
                assert result.success is False
                assert result.error_type in [ErrorType.CONNECTION, ErrorType.PARSE_ERROR]


def test_import_compatibility():
    """Test that module imports work correctly even without async dependencies."""
    # This test should always pass, testing import structure
    from earthcatalog.ingestion_pipeline import ProcessingConfig

    config = ProcessingConfig(
        input_file="/tmp/test.parquet", output_catalog="/tmp/test_catalog", scratch_location="/tmp/test_scratch"
    )

    # Should be able to create config even if async HTTP not available
    assert config.enable_concurrent_http is True  # Default should be True
    assert config.concurrent_requests == 50  # Default value
    assert config.batch_size == 1000  # Default value


def test_backward_compatibility():
    """Test that all existing functionality works unchanged."""
    import os
    import tempfile

    import pandas as pd

    # Create a temporary parquet file
    with tempfile.NamedTemporaryFile(suffix=".parquet", delete=False) as tmp_file:
        df = pd.DataFrame({"url": ["https://example.com/test.json"]})
        df.to_parquet(tmp_file.name)

        config = ProcessingConfig(
            input_file=tmp_file.name,
            output_catalog="/tmp/test_catalog",
            scratch_location="/tmp/test_scratch",
            enable_concurrent_http=False,  # Explicitly disable async
        )

        from earthcatalog.ingestion_pipeline import LocalProcessor

        processor = LocalProcessor()
        pipeline = STACIngestionPipeline(config, processor)

        # Should be able to create pipeline and call existing methods
        assert hasattr(pipeline, "_download_stac_item")
        assert hasattr(pipeline, "_compute_partition_key")

        # Configuration validation should work
        config.validate()

        # Clean up
        os.unlink(tmp_file.name)


@pytest.mark.skipif(not HAS_ASYNC_HTTP, reason="Async HTTP client not available")
class TestSessionLockAndCleanup:
    """Test session lock and cleanup improvements (c1, c2)."""

    @pytest.mark.asyncio
    async def test_session_lock_prevents_race_condition(self):
        """Test that session lock prevents race conditions during concurrent initialization."""
        client = AsyncHTTPClient(concurrent_requests=10)

        # Create multiple concurrent tasks that all try to enter the context
        async def enter_context():
            async with client:
                # Session should be properly initialized
                assert client.session is not None
                return True

        # Run multiple concurrent context manager entries
        # This would cause issues without the session lock
        tasks = [enter_context() for _ in range(5)]
        results = await asyncio.gather(*tasks, return_exceptions=True)

        # All tasks should complete successfully
        for result in results:
            assert result is True or isinstance(result, Exception)

    @pytest.mark.asyncio
    async def test_session_cleanup_on_normal_exit(self):
        """Test session is properly cleaned up on normal exit."""
        client = AsyncHTTPClient()

        async with client:
            assert client.session is not None

        # After exit, session should be None
        assert client.session is None

    @pytest.mark.asyncio
    async def test_session_cleanup_on_exception(self):
        """Test session is properly cleaned up even when exception occurs."""
        client = AsyncHTTPClient()

        class TestException(Exception):
            pass

        try:
            async with client:
                assert client.session is not None
                raise TestException("Test exception")
        except TestException:
            pass

        # Session should still be cleaned up
        assert client.session is None

    @pytest.mark.asyncio
    async def test_concurrent_session_access_with_lock(self):
        """Test that multiple coroutines can safely share a session."""

        async with AsyncHTTPClient(concurrent_requests=5) as client:
            # Verify session is created
            assert client.session is not None

            # Multiple coroutines accessing session should work safely
            async def check_session():
                return client.session is not None

            results = await asyncio.gather(*[check_session() for _ in range(10)])
            assert all(results)


@pytest.mark.skipif(not HAS_ASYNC_HTTP, reason="Async HTTP client not available")
class TestDownloadResultAndFailureTracking:
    """Test DownloadResult dataclass and failure tracking (c3)."""

    @pytest.mark.skipif(not HAS_ASYNC_TEST_SUPPORT, reason="aioresponses not available")
    @pytest.mark.asyncio
    async def test_download_result_structure(self):
        """Test DownloadResult dataclass has correct structure."""
        from earthcatalog.async_http_client import DownloadResult

        # Create a mock result
        result = DownloadResult(
            items=[{"id": "item1", "type": "Feature"}],
            failed_urls=[{"url": "http://fail.com", "error": "404", "error_type": "http_error"}],
            metrics={"total_requests": 2, "success_rate_percent": 50.0},
        )

        assert len(result.items) == 1
        assert len(result.failed_urls) == 1
        assert result.metrics["success_rate_percent"] == 50.0

    @pytest.mark.skipif(not HAS_ASYNC_TEST_SUPPORT, reason="aioresponses not available")
    @pytest.mark.asyncio
    async def test_download_with_failures_tracking(self):
        """Test that download_all_with_failures properly tracks failures."""
        from earthcatalog.async_http_client import BatchDownloader

        urls = [
            "https://example.com/success.json",
            "https://example.com/fail_404.json",
            "https://example.com/success2.json",
        ]

        with aioresponses() as mock_responses:
            # Mock responses
            mock_responses.get(urls[0], payload={"id": "item1", "type": "Feature"})
            mock_responses.get(urls[1], status=404)
            mock_responses.get(urls[2], payload={"id": "item2", "type": "Feature"})

            downloader = BatchDownloader(batch_size=10, concurrent_requests=5, request_timeout=5, retry_attempts=1)

            result = await downloader.download_all_with_failures(urls)

            # Should have 2 successful items
            assert len(result.items) == 2
            assert result.items[0]["id"] == "item1"
            assert result.items[1]["id"] == "item2"

            # Should have 1 failed URL with details
            assert len(result.failed_urls) == 1
            assert result.failed_urls[0]["url"] == urls[1]
            assert "error" in result.failed_urls[0]
            assert "error_type" in result.failed_urls[0]

            # Metrics should be populated
            assert "total_requests" in result.metrics
            assert "success_rate_percent" in result.metrics

    @pytest.mark.skipif(not HAS_ASYNC_TEST_SUPPORT, reason="aioresponses not available")
    @pytest.mark.asyncio
    async def test_download_stac_items_async_with_failures_function(self):
        """Test the convenience function download_stac_items_async_with_failures."""
        from earthcatalog.async_http_client import download_stac_items_async_with_failures

        urls = ["https://example.com/item1.json", "https://example.com/item2.json"]

        with aioresponses() as mock_responses:
            mock_responses.get(urls[0], payload={"id": "item1", "type": "Feature"})
            mock_responses.get(urls[1], status=500)  # Server error

            result = await download_stac_items_async_with_failures(
                urls=urls, concurrent_requests=5, batch_size=10, retry_attempts=1
            )

            assert isinstance(result.items, list)
            assert isinstance(result.failed_urls, list)
            assert isinstance(result.metrics, dict)


class TestGridParameterValidation:
    """Test grid parameter validation (h1)."""

    def test_h3_resolution_validation(self):
        """Test H3 resolution validation (0-15)."""
        # Valid resolutions
        H3GridSystem(resolution=0)
        H3GridSystem(resolution=15)
        H3GridSystem(resolution=6)

        # Invalid resolutions
        with pytest.raises(ValueError, match="H3 resolution must be 0-15"):
            H3GridSystem(resolution=-1)
        with pytest.raises(ValueError, match="H3 resolution must be 0-15"):
            H3GridSystem(resolution=16)

    def test_s2_resolution_validation(self):
        """Test S2 resolution validation (0-30)."""
        # Valid resolutions
        S2GridSystem(resolution=0)
        S2GridSystem(resolution=30)
        S2GridSystem(resolution=13)

        # Invalid resolutions
        with pytest.raises(ValueError, match="S2 resolution must be 0-30"):
            S2GridSystem(resolution=-1)
        with pytest.raises(ValueError, match="S2 resolution must be 0-30"):
            S2GridSystem(resolution=31)

    def test_mgrs_resolution_validation(self):
        """Test MGRS resolution validation (1-5)."""
        # Valid resolutions
        MGRSGridSystem(resolution=1)
        MGRSGridSystem(resolution=5)
        MGRSGridSystem(resolution=3)

        # Invalid resolutions
        with pytest.raises(ValueError, match="MGRS resolution must be 1-5"):
            MGRSGridSystem(resolution=0)
        with pytest.raises(ValueError, match="MGRS resolution must be 1-5"):
            MGRSGridSystem(resolution=6)

    def test_latlon_resolution_validation(self):
        """Test LatLon resolution validation (must be positive)."""
        # Valid resolutions
        SimpleLatLonGrid(resolution=1)
        SimpleLatLonGrid(resolution=10)

        # Invalid resolutions
        with pytest.raises(ValueError, match="LatLon resolution must be positive"):
            SimpleLatLonGrid(resolution=0)
        with pytest.raises(ValueError, match="LatLon resolution must be positive"):
            SimpleLatLonGrid(resolution=-1)


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
