import asyncio
import socket
import ssl
import threading
import time

import h2.connection
import h2.events
import httpx
import pytest

from asgiri.server import HttpProtocolVersion, Server

from .app import app


@pytest.fixture(scope="function")
def unused_port():
    """Find an unused port for testing."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind(("", 0))
        return s.getsockname()[1]


def wait_for_server(host: str, port: int, timeout: float = 5.0) -> bool:
    """Wait for server to be ready to accept connections."""
    start_time = time.time()
    while time.time() - start_time < timeout:
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
                sock.settimeout(0.5)
                sock.connect((host, port))
                return True
        except (ConnectionRefusedError, OSError):
            time.sleep(0.1)
    return False


@pytest.mark.parametrize(
    "protocol_version", [HttpProtocolVersion.HTTP_1_1, HttpProtocolVersion.AUTO]
)
@pytest.mark.timeout(5)
@pytest.mark.asyncio
async def test_get_helloworld_endpoint(
    protocol_version: HttpProtocolVersion, unused_port: int
):
    server = Server(
        app=app,
        host="127.0.0.1",
        port=unused_port,
        http_version=protocol_version,
    )

    # Start the server in a separate thread
    server_thread = threading.Thread(target=server.run, daemon=True)
    server_thread.start()

    # Wait for server to be ready
    assert wait_for_server("127.0.0.1", unused_port), "Server failed to start"

    # Make a request to the /helloworld endpoint
    response = httpx.get(f"http://127.0.0.1:{unused_port}/helloworld")
    assert response.status_code == 200
    assert response.json() == {"Hello": "World"}


@pytest.mark.parametrize(
    "protocol_version", [HttpProtocolVersion.HTTP_1_1, HttpProtocolVersion.AUTO]
)
@pytest.mark.timeout(5)
@pytest.mark.asyncio
async def test_post_echo_endpoint(
    protocol_version: HttpProtocolVersion, unused_port: int
):
    server = Server(
        app=app,
        host="127.0.0.1",
        port=unused_port,
        http_version=protocol_version,
    )

    # Start the server in a separate thread
    server_thread = threading.Thread(target=server.run, daemon=True)
    server_thread.start()

    # Wait for server to be ready
    assert wait_for_server("127.0.0.1", unused_port), "Server failed to start"

    # Make a request to the /echo endpoint
    response = httpx.post(
        f"http://127.0.0.1:{unused_port}/echo",
        json={"message": "Hello, World!"},
    )
    assert response.status_code == 200
    assert response.json() == {"message": "Hello, World!"}


@pytest.mark.parametrize(
    "protocol_version", [HttpProtocolVersion.HTTP_1_1, HttpProtocolVersion.AUTO]
)
@pytest.mark.timeout(5)
@pytest.mark.asyncio
async def test_get_with_parameters(
    protocol_version: HttpProtocolVersion, unused_port: int
):
    server = Server(
        app=app,
        host="127.0.0.1",
        port=unused_port,
        http_version=protocol_version,
    )

    # Start the server in a separate thread
    server_thread = threading.Thread(target=server.run, daemon=True)
    server_thread.start()

    # Wait for server to be ready
    assert wait_for_server("127.0.0.1", unused_port), "Server failed to start"

    # Make a request to the /helloworld endpoint
    response = httpx.get(
        f"http://127.0.0.1:{unused_port}/read_params?name=John&age=30&active=true"
    )
    assert response.status_code == 200
    assert response.json() == {"Name": "John", "Age": 30, "Active": True}


# ============================================================================
# HTTP/2 Specific Tests
# ============================================================================


@pytest.mark.timeout(5)
@pytest.mark.asyncio
async def test_http2_direct_connection(unused_port: int):
    """Test direct HTTP/2 connection using h2 library."""
    server = Server(
        app=app,
        host="127.0.0.1",
        port=unused_port,
        http_version=HttpProtocolVersion.AUTO,
    )

    # Start the server in a separate thread
    server_thread = threading.Thread(target=server.run, daemon=True)
    server_thread.start()

    # Wait for server to be ready
    assert wait_for_server("127.0.0.1", unused_port), "Server failed to start"

    # Create a raw socket connection
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.connect(("127.0.0.1", unused_port))

    # Create HTTP/2 connection
    config = h2.connection.H2Configuration(client_side=True)
    conn = h2.connection.H2Connection(config=config)
    conn.initiate_connection()
    sock.sendall(conn.data_to_send())

    # Send a GET request for /helloworld
    stream_id = conn.get_next_available_stream_id()
    headers = [
        (":method", "GET"),
        (":path", "/helloworld"),
        (":scheme", "http"),
        (":authority", f"127.0.0.1:{unused_port}"),
    ]
    conn.send_headers(stream_id, headers, end_stream=True)
    sock.sendall(conn.data_to_send())

    # Receive response
    response_data = b""
    response_headers = None
    data_received = False

    for _ in range(10):  # Try up to 10 times
        data = sock.recv(65536)
        if not data:
            break

        events = conn.receive_data(data)
        for event in events:
            if isinstance(event, h2.events.ResponseReceived):
                response_headers = dict(event.headers)
            elif isinstance(event, h2.events.DataReceived):
                response_data += event.data
                conn.acknowledge_received_data(
                    event.flow_controlled_length, event.stream_id
                )
                data_received = True
            elif isinstance(event, h2.events.StreamEnded):
                break

        sock.sendall(conn.data_to_send())

        if data_received:
            break

        time.sleep(0.1)

    sock.close()

    # Verify response
    assert response_headers is not None
    assert response_headers.get(b":status") == b"200"
    assert b'"Hello"' in response_data
    assert b'"World"' in response_data


@pytest.mark.timeout(5)
@pytest.mark.asyncio
async def test_http2_post_request(unused_port: int):
    """Test HTTP/2 POST request with body."""
    server = Server(
        app=app,
        host="127.0.0.1",
        port=unused_port,
        http_version=HttpProtocolVersion.AUTO,
    )

    # Start the server in a separate thread
    server_thread = threading.Thread(target=server.run, daemon=True)
    server_thread.start()

    # Wait for server to be ready
    assert wait_for_server("127.0.0.1", unused_port), "Server failed to start"

    # Create a raw socket connection
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.connect(("127.0.0.1", unused_port))

    # Create HTTP/2 connection
    config = h2.connection.H2Configuration(client_side=True)
    conn = h2.connection.H2Connection(config=config)
    conn.initiate_connection()
    sock.sendall(conn.data_to_send())

    # Send a POST request
    stream_id = conn.get_next_available_stream_id()
    body = b'{"test": "data"}'
    headers = [
        (":method", "POST"),
        (":path", "/echo"),
        (":scheme", "http"),
        (":authority", f"127.0.0.1:{unused_port}"),
        ("content-type", "application/json"),
        ("content-length", str(len(body))),
    ]
    conn.send_headers(stream_id, headers)
    sock.sendall(conn.data_to_send())

    conn.send_data(stream_id, body, end_stream=True)
    sock.sendall(conn.data_to_send())

    # Receive response
    response_data = b""
    response_headers = None
    data_received = False

    for _ in range(10):
        data = sock.recv(65536)
        if not data:
            break

        events = conn.receive_data(data)
        for event in events:
            if isinstance(event, h2.events.ResponseReceived):
                response_headers = dict(event.headers)
            elif isinstance(event, h2.events.DataReceived):
                response_data += event.data
                conn.acknowledge_received_data(
                    event.flow_controlled_length, event.stream_id
                )
                data_received = True
            elif isinstance(event, h2.events.StreamEnded):
                break

        sock.sendall(conn.data_to_send())

        if data_received:
            break

        time.sleep(0.1)

    sock.close()

    # Verify response
    assert response_headers is not None
    assert response_headers.get(b":status") == b"200"
    assert b'"test"' in response_data
    assert b'"data"' in response_data


@pytest.mark.timeout(5)
@pytest.mark.asyncio
async def test_http2_multiple_streams(unused_port: int):
    """Test HTTP/2 multiplexing with multiple concurrent streams."""
    server = Server(
        app=app,
        host="127.0.0.1",
        port=unused_port,
        http_version=HttpProtocolVersion.AUTO,
    )

    # Start the server in a separate thread
    server_thread = threading.Thread(target=server.run, daemon=True)
    server_thread.start()

    # Wait for server to be ready
    assert wait_for_server("127.0.0.1", unused_port), "Server failed to start"

    # Create a raw socket connection
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.connect(("127.0.0.1", unused_port))

    # Create HTTP/2 connection
    config = h2.connection.H2Configuration(client_side=True)
    conn = h2.connection.H2Connection(config=config)
    conn.initiate_connection()
    sock.sendall(conn.data_to_send())

    # Send multiple requests on different streams
    stream_ids = []
    for i in range(3):
        stream_id = conn.get_next_available_stream_id()
        stream_ids.append(stream_id)
        headers = [
            (":method", "GET"),
            (":path", "/helloworld"),
            (":scheme", "http"),
            (":authority", f"127.0.0.1:{unused_port}"),
        ]
        conn.send_headers(stream_id, headers, end_stream=True)
        sock.sendall(conn.data_to_send())

    # Receive responses
    streams_completed = set()
    response_count = 0

    for _ in range(30):  # Try up to 30 times
        data = sock.recv(65536)
        if not data:
            break

        events = conn.receive_data(data)
        for event in events:
            if isinstance(event, h2.events.DataReceived):
                conn.acknowledge_received_data(
                    event.flow_controlled_length, event.stream_id
                )
            elif isinstance(event, h2.events.StreamEnded):
                streams_completed.add(event.stream_id)
                response_count += 1

        sock.sendall(conn.data_to_send())

        if len(streams_completed) >= 3:
            break

        time.sleep(0.1)

    sock.close()

    # Verify all streams completed
    assert response_count == 3
    assert len(streams_completed) == 3


# ============================================================================
# Protocol Switching Tests
# ============================================================================


@pytest.mark.timeout(5)
@pytest.mark.asyncio
async def test_protocol_detection_http11(unused_port: int):
    """Test that AUTO protocol correctly detects and handles HTTP/1.1."""
    server = Server(
        app=app,
        host="127.0.0.1",
        port=unused_port,
        http_version=HttpProtocolVersion.AUTO,
    )

    # Start the server in a separate thread
    server_thread = threading.Thread(target=server.run, daemon=True)
    server_thread.start()

    # Wait for server to be ready
    assert wait_for_server("127.0.0.1", unused_port), "Server failed to start"

    # Create a raw socket and send HTTP/1.1 request
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.settimeout(2.0)  # Set socket timeout
    sock.connect(("127.0.0.1", unused_port))

    # Send HTTP/1.1 request
    request = b"GET /helloworld HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"
    sock.sendall(request)

    # Receive response
    response = b""
    try:
        while True:
            data = sock.recv(4096)
            if not data:
                break
            response += data
    except socket.timeout:
        pass  # Timeout is expected when connection closes

    sock.close()

    # Verify HTTP/1.1 response
    assert b"HTTP/1.1 200" in response
    assert b'"Hello"' in response
    assert b'"World"' in response


@pytest.mark.timeout(5)
@pytest.mark.asyncio
async def test_protocol_detection_http2(unused_port: int):
    """Test that AUTO protocol correctly detects and handles HTTP/2."""
    server = Server(
        app=app,
        host="127.0.0.1",
        port=unused_port,
        http_version=HttpProtocolVersion.AUTO,
    )

    # Start the server in a separate thread
    server_thread = threading.Thread(target=server.run, daemon=True)
    server_thread.start()

    # Wait for server to be ready
    assert wait_for_server("127.0.0.1", unused_port), "Server failed to start"

    # Create a raw socket and send HTTP/2 preface
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.settimeout(2.0)  # Set socket timeout
    sock.connect(("127.0.0.1", unused_port))

    # Create HTTP/2 connection
    config = h2.connection.H2Configuration(client_side=True)
    conn = h2.connection.H2Connection(config=config)
    conn.initiate_connection()

    # Send HTTP/2 preface and settings
    sock.sendall(conn.data_to_send())

    # Wait for server response
    data = sock.recv(4096)
    events = conn.receive_data(data)

    # Verify we got HTTP/2 settings frame back
    settings_received = any(
        isinstance(e, h2.events.RemoteSettingsChanged) for e in events
    )
    assert settings_received, "Server should respond with HTTP/2 settings"

    sock.close()


@pytest.mark.timeout(5)
@pytest.mark.asyncio
async def test_sequential_http11_and_http2_connections(unused_port: int):
    """Test that AUTO protocol can handle HTTP/1.1 and HTTP/2 connections sequentially."""
    server = Server(
        app=app,
        host="127.0.0.1",
        port=unused_port,
        http_version=HttpProtocolVersion.AUTO,
    )

    # Start the server in a separate thread
    server_thread = threading.Thread(target=server.run, daemon=True)
    server_thread.start()

    # Wait for server to be ready
    assert wait_for_server("127.0.0.1", unused_port), "Server failed to start"

    # First, make an HTTP/1.1 request
    sock1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock1.settimeout(2.0)  # Set socket timeout
    sock1.connect(("127.0.0.1", unused_port))
    request = b"GET /helloworld HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"
    sock1.sendall(request)

    response1 = b""
    try:
        while True:
            data = sock1.recv(4096)
            if not data:
                break
            response1 += data
    except socket.timeout:
        pass  # Timeout is expected when connection closes
    sock1.close()

    assert b"HTTP/1.1 200" in response1

    # Then, make an HTTP/2 request
    sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock2.settimeout(2.0)  # Set socket timeout
    sock2.connect(("127.0.0.1", unused_port))

    config = h2.connection.H2Configuration(client_side=True)
    conn = h2.connection.H2Connection(config=config)
    conn.initiate_connection()
    sock2.sendall(conn.data_to_send())

    stream_id = conn.get_next_available_stream_id()
    headers = [
        (":method", "GET"),
        (":path", "/helloworld"),
        (":scheme", "http"),
        (":authority", f"127.0.0.1:{unused_port}"),
    ]
    conn.send_headers(stream_id, headers, end_stream=True)
    sock2.sendall(conn.data_to_send())

    # Receive HTTP/2 response
    response_data = b""
    for _ in range(10):
        try:
            data = sock2.recv(65536)
            if not data:
                break

            events = conn.receive_data(data)
            for event in events:
                if isinstance(event, h2.events.DataReceived):
                    response_data += event.data
                    conn.acknowledge_received_data(
                        event.flow_controlled_length, event.stream_id
                    )
                    break

            sock2.sendall(conn.data_to_send())

            if response_data:
                break

            time.sleep(0.1)
        except socket.timeout:
            break

    sock2.close()

    assert b'"Hello"' in response_data
    assert b'"World"' in response_data


@pytest.mark.timeout(5)
@pytest.mark.asyncio
async def test_http2_with_httpx_client(unused_port: int):
    """Test HTTP/2 using httpx with http2=True."""
    server = Server(
        app=app,
        host="127.0.0.1",
        port=unused_port,
        http_version=HttpProtocolVersion.HTTP_2,
    )

    # Start the server in a separate thread
    server_thread = threading.Thread(target=server.run, daemon=True)
    server_thread.start()

    # Wait for server to be ready
    assert wait_for_server("127.0.0.1", unused_port), "Server failed to start"

    # httpx requires HTTP/2 over TLS in most cases, so this test uses raw h2
    # But we can test that the endpoint works
    async with httpx.AsyncClient(http2=True) as client:
        try:
            # Note: httpx may not actually use HTTP/2 without TLS in practice
            response = await client.get(
                f"http://127.0.0.1:{unused_port}/helloworld"
            )
            assert response.status_code == 200
            assert response.json() == {"Hello": "World"}
        except Exception:
            # If httpx doesn't support plain HTTP/2, that's expected
            pass
