"""TLS behaviour tests for ``create_channel``.

These spin up a real loopback ``grpc.aio`` server with a freshly generated
self-signed certificate (carrying a ``localhost`` SAN, mirroring the gateway's
auto-generated cert) and assert the lenient TOFU default lets a client connect
without any CA configured.

Marked ``tls`` and skipped unless ``MXGATEWAY_RUN_TLS_TESTS=1`` because loopback
TLS handshakes can be timing-flaky on shared CI runners. This mirrors how the
suite gates anything that depends on real sockets rather than fakes.
"""

from __future__ import annotations

import os
import shutil
import socket
import ssl
import subprocess
import tempfile
from collections.abc import AsyncIterator
from pathlib import Path

import grpc
import pytest
import pytest_asyncio

from zb_mom_ww_mxgateway import ClientOptions
from zb_mom_ww_mxgateway.errors import MxGatewayTransportError
from zb_mom_ww_mxgateway.generated import mxaccess_gateway_pb2 as pb
from zb_mom_ww_mxgateway.generated import mxaccess_gateway_pb2_grpc as pb_grpc
from zb_mom_ww_mxgateway.options import create_channel

pytestmark = pytest.mark.tls

_RUN_TLS_TESTS = os.environ.get("MXGATEWAY_RUN_TLS_TESTS") == "1"
_OPENSSL = shutil.which("openssl")

requires_tls = pytest.mark.skipif(
    not _RUN_TLS_TESTS,
    reason="set MXGATEWAY_RUN_TLS_TESTS=1 to run loopback TLS tests",
)
requires_openssl = pytest.mark.skipif(
    _OPENSSL is None,
    reason="openssl CLI is required to generate a self-signed test certificate",
)


def _generate_self_signed_cert(directory: Path) -> tuple[Path, Path]:
    """Generate a self-signed cert/key pair with a ``localhost`` SAN."""
    key_path = directory / "server.key"
    cert_path = directory / "server.crt"
    subprocess.run(
        [
            str(_OPENSSL),
            "req",
            "-x509",
            "-newkey",
            "rsa:2048",
            "-nodes",
            "-keyout",
            str(key_path),
            "-out",
            str(cert_path),
            "-days",
            "1",
            "-subj",
            "/CN=mxgateway-test",
            "-addext",
            "subjectAltName=DNS:localhost,IP:127.0.0.1",
        ],
        check=True,
        capture_output=True,
    )
    return cert_path, key_path


def _free_port() -> int:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.bind(("127.0.0.1", 0))
        return int(sock.getsockname()[1])


class _StaticGatewayServicer(pb_grpc.MxAccessGatewayServicer):
    """Minimal servicer answering ``OpenSession`` with a fixed session id."""

    async def OpenSession(  # noqa: N802 - generated gRPC method name
        self, request: pb.OpenSessionRequest, context: object
    ) -> pb.OpenSessionReply:
        return pb.OpenSessionReply(session_id="tls-session-1")


@pytest_asyncio.fixture
async def tls_server() -> AsyncIterator[int]:
    with tempfile.TemporaryDirectory() as tmp:
        cert_path, key_path = _generate_self_signed_cert(Path(tmp))
        credentials = grpc.ssl_server_credentials(
            [(key_path.read_bytes(), cert_path.read_bytes())]
        )
        server = grpc.aio.server()
        pb_grpc.add_MxAccessGatewayServicer_to_server(_StaticGatewayServicer(), server)
        port = _free_port()
        server.add_secure_port(f"127.0.0.1:{port}", credentials)
        await server.start()
        try:
            yield port
        finally:
            await server.stop(grace=None)


@requires_tls
@requires_openssl
@pytest.mark.asyncio
async def test_default_tls_connects_via_tofu(tls_server: int) -> None:
    """Default TLS options (no CA) connect by pinning the presented cert."""
    options = ClientOptions(
        endpoint=f"127.0.0.1:{tls_server}",
        api_key="mxgw_test_secret",
    )
    channel = create_channel(options)
    try:
        stub = pb_grpc.MxAccessGatewayStub(channel)
        reply = await stub.OpenSession(pb.OpenSessionRequest(), timeout=10)
        assert reply.session_id == "tls-session-1"
    finally:
        await channel.close()


def test_split_authority_parses_host_and_port() -> None:
    from zb_mom_ww_mxgateway.options import _split_authority

    assert _split_authority("https://10.0.0.5:5120") == ("10.0.0.5", 5120)
    assert _split_authority("localhost:5120") == ("localhost", 5120)
    assert _split_authority(":5120") == ("localhost", 5120)


def test_split_authority_defaults_port_for_portless_endpoint() -> None:
    from zb_mom_ww_mxgateway.options import _split_authority

    # A bare hostname (no ":port") must default to 443, not crash on int("mygateway").
    assert _split_authority("mygateway") == ("mygateway", 443)
    # Scheme-prefixed bare hostname behaves the same.
    assert _split_authority("https://mygateway") == ("mygateway", 443)
    # A non-numeric tail after a colon is treated as no explicit port.
    assert _split_authority("mygateway:") == ("mygateway", 443)


def test_split_authority_strips_ipv6_brackets() -> None:
    from zb_mom_ww_mxgateway.options import _split_authority

    # Bracketed IPv6 with port — brackets must be removed for ssl.get_server_certificate
    assert _split_authority("[::1]:5120") == ("::1", 5120)
    # Bare bracketed IPv6 (no port) — default port 443
    assert _split_authority("[::1]") == ("::1", 443)
    # Scheme-prefixed bracketed IPv6
    assert _split_authority("grpc://[::1]:5120") == ("::1", 5120)


def test_tofu_connect_failure_raises_transport_error() -> None:
    """A failed cert pre-fetch surfaces the client's transport error type."""
    options = ClientOptions(endpoint=f"127.0.0.1:{_free_port()}")
    with pytest.raises(MxGatewayTransportError) as excinfo:
        create_channel(options)
    assert options.endpoint in str(excinfo.value)


def test_require_certificate_validation_uses_system_trust() -> None:
    """``require_certificate_validation`` must not attempt a TOFU pre-fetch."""
    # Pointing at a closed port: with system-trust the channel is created lazily
    # (no eager pre-fetch), so create_channel must succeed without connecting.
    options = ClientOptions(
        endpoint=f"127.0.0.1:{_free_port()}",
        require_certificate_validation=True,
    )
    channel = create_channel(options)
    assert isinstance(channel, grpc.aio.Channel)
