from __future__ import annotations

import os
import socket
import ssl
import sys
from unittest.mock import Mock, NonCallableMock

import anycorn.config
import pytest
from _pytest.monkeypatch import MonkeyPatch
from anycorn.config import Config

access_log_format = "bob"
h11_max_incomplete_size = 4


def _check_standard_config(config: Config) -> None:
    assert config.access_log_format == access_log_format
    assert config.h11_max_incomplete_size == h11_max_incomplete_size
    assert config.bind == ["127.0.0.1:5555"]


def test_config_from_pyfile() -> None:
    path = os.path.join(os.path.dirname(__file__), "assets/config.py")
    config = Config.from_pyfile(path)
    _check_standard_config(config)


def test_config_from_object() -> None:
    sys.path.append(os.path.join(os.path.dirname(__file__)))

    config = Config.from_object("assets.config")
    _check_standard_config(config)


def test_ssl_config_from_pyfile() -> None:
    path = os.path.join(os.path.dirname(__file__), "assets/config_ssl.py")
    config = Config.from_pyfile(path)
    _check_standard_config(config)
    assert config.ssl_enabled


def test_config_from_toml() -> None:
    path = os.path.join(os.path.dirname(__file__), "assets/config.toml")
    config = Config.from_toml(path)
    _check_standard_config(config)


def test_create_ssl_context() -> None:
    path = os.path.join(os.path.dirname(__file__), "assets/config_ssl.py")
    config = Config.from_pyfile(path)
    context = config.create_ssl_context()

    # NOTE: In earlier versions of python context.options is equal to <Options.OP_NO_COMPRESSION: 0>
    #       hence the ANDing context.options with the specified ssl options results in
    #       "<Options.OP_NO_COMPRESSION: 0>", which as a Boolean value, is False.
    #
    #       To overcome this, instead of checking that the result in True, we will check that it is
    #        equal to "context.options".
    assert (
        context.options
        & (
            ssl.OP_NO_SSLv2
            | ssl.OP_NO_SSLv3
            | ssl.OP_NO_TLSv1
            | ssl.OP_NO_TLSv1_1
            | ssl.OP_NO_COMPRESSION
        )
        == context.options
    )


@pytest.mark.parametrize(
    "bind, expected_family, expected_binding",
    [
        ("127.0.0.1:5000", socket.AF_INET, ("127.0.0.1", 5000)),
        ("127.0.0.1", socket.AF_INET, ("127.0.0.1", 8000)),
        ("[::]:5000", socket.AF_INET6, ("::", 5000)),
        ("[::]", socket.AF_INET6, ("::", 8000)),
    ],
)
def test_create_sockets_ip(
    bind: str,
    expected_family: socket.AddressFamily,
    expected_binding: tuple[str, int],
    monkeypatch: MonkeyPatch,
) -> None:
    mock_socket = Mock()
    monkeypatch.setattr(socket, "socket", mock_socket)
    config = Config()
    config.bind = [bind]
    sockets = config.create_sockets()
    sock = sockets.insecure_sockets[0]
    mock_socket.assert_called_with(expected_family, socket.SOCK_STREAM)
    sock.setsockopt.assert_called_with(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # type: ignore[attr-defined]
    sock.bind.assert_called_with(expected_binding)  # type: ignore[attr-defined]
    sock.setblocking.assert_called_with(False)  # type: ignore[attr-defined]
    sock.set_inheritable.assert_called_with(True)  # type: ignore[attr-defined]


@pytest.mark.skipif(sys.platform == "win32", reason="Windows is not Unix.")
def test_create_sockets_unix(monkeypatch: MonkeyPatch) -> None:
    mock_socket = Mock()
    monkeypatch.setattr(socket, "socket", mock_socket)
    monkeypatch.setattr(os, "chown", Mock())
    config = Config()
    config.bind = ["unix:/tmp/anycorn.sock"]
    sockets = config.create_sockets()
    sock = sockets.insecure_sockets[0]
    mock_socket.assert_called_with(socket.AF_UNIX, socket.SOCK_STREAM)
    sock.setsockopt.assert_called_with(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # type: ignore[attr-defined]
    sock.bind.assert_called_with("/tmp/anycorn.sock")  # type: ignore[attr-defined]
    sock.setblocking.assert_called_with(False)  # type: ignore[attr-defined]
    sock.set_inheritable.assert_called_with(True)  # type: ignore[attr-defined]


def test_create_sockets_fd(monkeypatch: MonkeyPatch) -> None:
    mock_sock_class = Mock(
        return_value=NonCallableMock(**{"getsockopt.return_value": socket.SOCK_STREAM})  # type: ignore[arg-type]
    )
    monkeypatch.setattr(socket, "socket", mock_sock_class)
    config = Config()
    config.bind = ["fd://2"]
    sockets = config.create_sockets()
    sock = sockets.insecure_sockets[0]
    mock_sock_class.assert_called_with(fileno=2)
    sock.getsockopt.assert_called_with(socket.SOL_SOCKET, socket.SO_TYPE)  # type: ignore[attr-defined]
    sock.setsockopt.assert_called_with(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)  # type: ignore[attr-defined]
    sock.setblocking.assert_called_with(False)  # type: ignore[attr-defined]
    sock.set_inheritable.assert_called_with(True)  # type: ignore[attr-defined]


@pytest.mark.skipif(sys.platform == "win32", reason="Windows is not Unix.")
def test_create_sockets_multiple(monkeypatch: MonkeyPatch) -> None:
    mock_socket = Mock()
    monkeypatch.setattr(socket, "socket", mock_socket)
    monkeypatch.setattr(os, "chown", Mock())
    config = Config()
    config.bind = ["127.0.0.1", "unix:/tmp/anycorn.sock"]
    sockets = config.create_sockets()
    assert len(sockets.insecure_sockets) == 2


def test_response_headers(monkeypatch: MonkeyPatch) -> None:
    monkeypatch.setattr(anycorn.config, "time", lambda: 1_512_229_395)
    config = Config()
    assert config.response_headers("test") == [
        (b"date", b"Sat, 02 Dec 2017 15:43:15 GMT"),
        (b"server", b"anycorn-test"),
    ]
    config.include_server_header = False
    assert config.response_headers("test") == [(b"date", b"Sat, 02 Dec 2017 15:43:15 GMT")]
