#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.

"""Pytest fixtures for ASGI compliance Docker integration tests."""

import subprocess
import time
import socket
from pathlib import Path

import pytest

# Directory containing this conftest.py
DOCKER_DIR = Path(__file__).parent
CERTS_DIR = DOCKER_DIR / "certs"


def generate_self_signed_cert(certs_dir: Path) -> None:
    """Generate self-signed SSL certificates for testing."""
    certs_dir.mkdir(parents=True, exist_ok=True)
    cert_file = certs_dir / "server.crt"
    key_file = certs_dir / "server.key"

    # Skip if certs already exist and are recent (less than 1 day old)
    if cert_file.exists() and key_file.exists():
        age = time.time() - cert_file.stat().st_mtime
        if age < 86400:  # 1 day
            return

    # Generate self-signed certificate
    subprocess.run(
        [
            "openssl", "req", "-x509", "-newkey", "rsa:2048",
            "-keyout", str(key_file),
            "-out", str(cert_file),
            "-days", "1",
            "-nodes",
            "-subj", "/CN=localhost/O=Gunicorn Test/C=US",
            "-addext", "subjectAltName=DNS:localhost,DNS:gunicorn-asgi,DNS:gunicorn-asgi-ssl,IP:127.0.0.1"
        ],
        check=True,
        capture_output=True
    )
    # Set readable permissions
    cert_file.chmod(0o644)
    key_file.chmod(0o644)


def wait_for_http_service(host: str, port: int, timeout: int = 60) -> bool:
    """Wait for an HTTP service to become available."""
    start_time = time.time()
    while time.time() - start_time < timeout:
        try:
            with socket.create_connection((host, port), timeout=5):
                return True
        except (socket.error, OSError):
            time.sleep(1)
    return False


def wait_for_https_service(host: str, port: int, timeout: int = 60) -> bool:
    """Wait for an HTTPS service to become available."""
    import ssl

    start_time = time.time()
    while time.time() - start_time < timeout:
        try:
            ctx = ssl.create_default_context()
            ctx.check_hostname = False
            ctx.verify_mode = ssl.CERT_NONE

            with socket.create_connection((host, port), timeout=5) as sock:
                with ctx.wrap_socket(sock, server_hostname=host):
                    return True
        except (socket.error, ssl.SSLError, OSError):
            time.sleep(1)
    return False


@pytest.fixture(scope="session")
def docker_compose_file():
    """Return the path to docker-compose.yml."""
    return DOCKER_DIR / "docker-compose.yml"


@pytest.fixture(scope="session")
def certs_dir():
    """Generate and return the certs directory."""
    generate_self_signed_cert(CERTS_DIR)
    return CERTS_DIR


@pytest.fixture(scope="session")
def docker_services(docker_compose_file, certs_dir):
    """Start Docker services for the test session."""
    compose_file = str(docker_compose_file)

    # Check if Docker is available
    try:
        subprocess.run(
            ["docker", "info"],
            check=True,
            capture_output=True
        )
    except (subprocess.CalledProcessError, FileNotFoundError):
        pytest.skip("Docker is not available")

    # Check if docker compose is available
    try:
        subprocess.run(
            ["docker", "compose", "version"],
            check=True,
            capture_output=True
        )
    except subprocess.CalledProcessError:
        pytest.skip("Docker Compose is not available")

    # Build and start services
    try:
        subprocess.run(
            ["docker", "compose", "-f", compose_file, "build"],
            check=True,
            cwd=DOCKER_DIR
        )
        subprocess.run(
            ["docker", "compose", "-f", compose_file, "up", "-d"],
            check=True,
            cwd=DOCKER_DIR
        )

        # Wait for services to be healthy
        gunicorn_http_ready = wait_for_http_service("127.0.0.1", 8000, timeout=60)
        gunicorn_https_ready = wait_for_https_service("127.0.0.1", 8445, timeout=60)
        nginx_http_ready = wait_for_http_service("127.0.0.1", 8080, timeout=60)
        nginx_https_ready = wait_for_https_service("127.0.0.1", 8444, timeout=60)

        if not gunicorn_http_ready:
            result = subprocess.run(
                ["docker", "compose", "-f", compose_file, "logs", "gunicorn-asgi"],
                capture_output=True,
                text=True,
                cwd=DOCKER_DIR
            )
            pytest.fail(f"Gunicorn HTTP service failed to start. Logs:\n{result.stdout}\n{result.stderr}")

        if not gunicorn_https_ready:
            result = subprocess.run(
                ["docker", "compose", "-f", compose_file, "logs", "gunicorn-asgi-ssl"],
                capture_output=True,
                text=True,
                cwd=DOCKER_DIR
            )
            pytest.fail(f"Gunicorn HTTPS service failed to start. Logs:\n{result.stdout}\n{result.stderr}")

        if not nginx_http_ready or not nginx_https_ready:
            result = subprocess.run(
                ["docker", "compose", "-f", compose_file, "logs", "nginx-proxy"],
                capture_output=True,
                text=True,
                cwd=DOCKER_DIR
            )
            pytest.fail(f"Nginx service failed to start. Logs:\n{result.stdout}\n{result.stderr}")

        yield {
            "gunicorn_http": "http://127.0.0.1:8000",
            "gunicorn_https": "https://127.0.0.1:8445",
            "nginx_http": "http://127.0.0.1:8080",
            "nginx_https": "https://127.0.0.1:8444",
        }

    finally:
        # Stop and remove services
        subprocess.run(
            ["docker", "compose", "-f", compose_file, "down", "-v", "--remove-orphans"],
            cwd=DOCKER_DIR,
            capture_output=True
        )


# ============================================================================
# URL Fixtures
# ============================================================================

@pytest.fixture
def gunicorn_url(docker_services):
    """Return the gunicorn HTTP service URL."""
    return docker_services["gunicorn_http"]


@pytest.fixture
def gunicorn_ssl_url(docker_services):
    """Return the gunicorn HTTPS service URL."""
    return docker_services["gunicorn_https"]


@pytest.fixture
def nginx_url(docker_services):
    """Return the nginx HTTP proxy URL."""
    return docker_services["nginx_http"]


@pytest.fixture
def nginx_ssl_url(docker_services):
    """Return the nginx HTTPS proxy URL."""
    return docker_services["nginx_https"]


# ============================================================================
# HTTP Client Fixtures
# ============================================================================

@pytest.fixture
def http_client():
    """Create a standard HTTP client."""
    httpx = pytest.importorskip("httpx")
    client = httpx.Client(verify=False, timeout=30.0, follow_redirects=False)
    yield client
    client.close()


@pytest.fixture
def http2_client():
    """Create an HTTP/2 capable client."""
    httpx = pytest.importorskip("httpx")
    client = httpx.Client(http2=True, verify=False, timeout=30.0)
    yield client
    client.close()


@pytest.fixture
async def async_http_client():
    """Create an async HTTP client."""
    httpx = pytest.importorskip("httpx")
    async with httpx.AsyncClient(verify=False, timeout=30.0) as client:
        yield client


@pytest.fixture
def async_http_client_factory():
    """Factory for creating async HTTP clients."""
    httpx = pytest.importorskip("httpx")

    async def create_client(**kwargs):
        defaults = {"verify": False, "timeout": 30.0}
        defaults.update(kwargs)
        return httpx.AsyncClient(**defaults)

    return create_client


# ============================================================================
# WebSocket Client Fixtures
# ============================================================================

@pytest.fixture
def websocket_connect():
    """Factory for creating WebSocket connections."""
    websockets = pytest.importorskip("websockets")

    async def connect(url, **kwargs):
        """Connect to a WebSocket endpoint.

        Args:
            url: WebSocket URL (ws:// or wss://)
            **kwargs: Additional arguments for websockets.connect()

        Returns:
            WebSocket connection
        """
        import ssl

        # Default SSL context for wss://
        if url.startswith("wss://") and "ssl" not in kwargs:
            ssl_context = ssl.create_default_context()
            ssl_context.check_hostname = False
            ssl_context.verify_mode = ssl.CERT_NONE
            kwargs["ssl"] = ssl_context

        return await websockets.connect(url, **kwargs)

    return connect


# ============================================================================
# Streaming Client Fixtures
# ============================================================================

@pytest.fixture
def sse_client():
    """Create a client for Server-Sent Events."""
    httpx = pytest.importorskip("httpx")

    class SSEClient:
        def __init__(self):
            self.client = httpx.Client(verify=False, timeout=60.0)

        def stream(self, url):
            """Stream SSE events from URL."""
            with self.client.stream("GET", url, headers={"Accept": "text/event-stream"}) as response:
                buffer = ""
                for chunk in response.iter_text():
                    buffer += chunk
                    while "\n\n" in buffer:
                        event, buffer = buffer.split("\n\n", 1)
                        yield self._parse_event(event)

        def _parse_event(self, event_text):
            """Parse an SSE event."""
            event = {"data": None, "event": None, "id": None}
            for line in event_text.strip().split("\n"):
                if line.startswith("data: "):
                    event["data"] = line[6:]
                elif line.startswith("event: "):
                    event["event"] = line[7:]
                elif line.startswith("id: "):
                    event["id"] = line[4:]
            return event

        def close(self):
            self.client.close()

    client = SSEClient()
    yield client
    client.close()


@pytest.fixture
def streaming_client():
    """Create a client for chunked/streaming responses."""
    httpx = pytest.importorskip("httpx")

    class StreamingClient:
        def __init__(self):
            self.client = httpx.Client(verify=False, timeout=60.0)

        def stream_chunks(self, url, method="GET", **kwargs):
            """Stream response chunks from URL."""
            with self.client.stream(method, url, **kwargs) as response:
                for chunk in response.iter_bytes():
                    if chunk:
                        yield chunk

        def stream_lines(self, url, method="GET", **kwargs):
            """Stream response lines from URL."""
            with self.client.stream(method, url, **kwargs) as response:
                for line in response.iter_lines():
                    yield line

        def close(self):
            self.client.close()

    client = StreamingClient()
    yield client
    client.close()


# ============================================================================
# Test Markers
# ============================================================================

def pytest_configure(config):
    """Configure custom pytest markers."""
    config.addinivalue_line("markers", "docker: tests requiring Docker")
    config.addinivalue_line("markers", "asgi: ASGI-related tests")
    config.addinivalue_line("markers", "websocket: WebSocket tests")
    config.addinivalue_line("markers", "streaming: Streaming response tests")
    config.addinivalue_line("markers", "lifespan: Lifespan protocol tests")
    config.addinivalue_line("markers", "framework: Framework integration tests")
    config.addinivalue_line("markers", "concurrency: Concurrency tests")
    config.addinivalue_line("markers", "http2: HTTP/2 specific tests")
    config.addinivalue_line("markers", "ssl: SSL/TLS tests")
    config.addinivalue_line("markers", "integration: Integration tests")
