import logging
import time
import ssl
import socket
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict

import pytest
import requests
from requests.adapters import HTTPAdapter
from requests.cookies import cookiejar_from_dict
from urllib3.util import parse_url

from pyrelukko import RelukkoClient
from pyrelukko.pyrelukko import RelukkoDoRetry
from pyrelukko.retry import retry


SCRIPT_DIR = Path(__file__).parent.absolute()

logger = logging.getLogger(__name__)


@retry(None, ConnectionRefusedError)
def _check_tls(ssl_ctx: ssl.SSLContext, port):
    with socket.create_connection(("127.0.0.1", port)) as sock:
        with ssl_ctx.wrap_socket(sock, server_hostname="relukko") as ssock:
            response = ssock.recv(20)
            assert response == b"Hello, TLS Client!"


def _check_has_serial_no(ssl_ctx: ssl.SSLContext):
    serial_numbers = [x["serialNumber"] for x in ssl_ctx.get_ca_certs()]
    assert "4573816C13FE0529EAD797EAB28DFED16930B76B" in serial_numbers


def test_init_relukko_client():
    relukko = RelukkoClient(base_url="", api_key="")

    assert isinstance(relukko, RelukkoClient)
    assert relukko.session.trust_env == True
    assert relukko.session.cookies._cookies == {}
    assert relukko.ssl_ctx is None
    http_adapter: HTTPAdapter = relukko.session.adapters.get("http://")
    assert http_adapter.max_retries.total == 0
    assert http_adapter.max_retries.connect is None
    assert http_adapter.max_retries.read == False
    assert http_adapter.max_retries.redirect is None
    assert http_adapter.max_retries.status is None
    assert http_adapter.max_retries.other is None
    assert http_adapter.max_retries.backoff_factor == 0
    assert http_adapter.max_retries.backoff_max == 120
    assert http_adapter.max_retries.backoff_jitter == 0.0
    assert http_adapter.max_retries.raise_on_redirect == True
    assert http_adapter.max_retries.raise_on_status == True
    assert relukko.tries == 4
    assert relukko.delay == 5
    assert relukko.backoff == 2.0
    assert relukko.max_delay is None
    assert relukko.exceptions == (requests.ConnectionError, RelukkoDoRetry)

    with pytest.raises(ValueError):
        RelukkoClient(base_url=1000, api_key="")

def test_init_relukko_client_with_env_var(tls_listener):
    _, port = tls_listener

    capath = SCRIPT_DIR / "cert"
    cafile = capath / "rootCA.crt"

    pytest.MonkeyPatch().setenv("REQUESTS_CA_BUNDLE", str(cafile.absolute()))
    relukko = RelukkoClient(base_url="https://relukko", api_key="")
    pytest.MonkeyPatch().delenv("REQUESTS_CA_BUNDLE")
    _check_tls(relukko.ssl_ctx, port)
    _check_has_serial_no(relukko.ssl_ctx)

    pytest.MonkeyPatch().setenv("CURL_CA_BUNDLE", str(cafile.absolute()))
    relukko2 = RelukkoClient(base_url="https://relukko", api_key="")
    pytest.MonkeyPatch().delenv("CURL_CA_BUNDLE")
    _check_tls(relukko2.ssl_ctx, port)
    _check_has_serial_no(relukko2.ssl_ctx)

    pytest.MonkeyPatch().setenv("REQUESTS_CA_BUNDLE", str(capath.absolute()))
    relukko3 = RelukkoClient(base_url="https://relukko", api_key="")
    pytest.MonkeyPatch().delenv("REQUESTS_CA_BUNDLE")
    _check_tls(relukko3.ssl_ctx, port)
    _check_has_serial_no(relukko3.ssl_ctx)

    pytest.MonkeyPatch().setenv("CURL_CA_BUNDLE", str(capath.absolute()))
    relukko4 = RelukkoClient(base_url="https://relukko", api_key="")
    pytest.MonkeyPatch().delenv("CURL_CA_BUNDLE")
    _check_tls(relukko4.ssl_ctx, port)
    _check_has_serial_no(relukko4.ssl_ctx)


def test_init_relukko_client_with_cafile(tls_listener):
    _, port = tls_listener

    cafile = SCRIPT_DIR / "cert" / "rootCA.crt"

    relukko = RelukkoClient(
        base_url="https://relukko", api_key="", cafile=cafile)
    _check_tls(relukko.ssl_ctx, port)
    _check_has_serial_no(relukko.ssl_ctx)


def test_init_relukko_client_with_capath(tls_listener):
    _, port = tls_listener

    capath = SCRIPT_DIR / "cert"

    relukko = RelukkoClient(
        base_url="https://relukko", api_key="", capath=capath)
    _check_tls(relukko.ssl_ctx, port)
    _check_has_serial_no(relukko.ssl_ctx)


def test_init_relukko_client_with_cadata(tls_listener):
    _, port = tls_listener

    cafile = SCRIPT_DIR / "cert" / "rootCA.crt"
    cadata = cafile.read_text()

    relukko = RelukkoClient(
        base_url="https://relukko", api_key="", cadata=cadata)
    _check_tls(relukko.ssl_ctx, port)
    _check_has_serial_no(relukko.ssl_ctx)

    cader = SCRIPT_DIR / "cert" / "rootCA.der"
    cadata = cader.read_bytes()

    relukko2 = RelukkoClient(
        base_url="https://relukko", api_key="", cadata=cadata)
    _check_tls(relukko2.ssl_ctx, port)
    _check_has_serial_no(relukko2.ssl_ctx)


def test_check_tls_throws_error(tls_listener):
    _, port = tls_listener

    relukko = RelukkoClient(
        base_url="https://relukko", api_key="")
    with pytest.raises(ssl.SSLCertVerificationError):
        _check_tls(relukko.ssl_ctx, port)


def test_reconfigure_relukko_client():
    relukko = RelukkoClient(base_url="", api_key="")

    relukko.reconfigure_relukko(
        "https://relukko", api_key="secret-key")

    assert relukko.base_url == parse_url("https://relukko")
    assert relukko.ws_url == parse_url("wss://relukko/ws/broadcast")
    assert relukko.session.headers['X-api-Key'] == "secret-key"

    default_ctx = ssl.create_default_context()
    assert relukko.ssl_ctx.check_hostname == default_ctx.check_hostname
    assert relukko.ssl_ctx.hostname_checks_common_name == \
        default_ctx.hostname_checks_common_name
    assert relukko.ssl_ctx.keylog_filename == default_ctx.keylog_filename
    assert relukko.ssl_ctx.sni_callback == default_ctx.sni_callback
    assert relukko.ssl_ctx.verify_flags == default_ctx.verify_flags
    assert relukko.ssl_ctx.verify_mode == default_ctx.verify_mode
    assert relukko.ssl_ctx.options == default_ctx.options
    assert relukko.tries == 4
    assert relukko.delay == 5
    assert relukko.backoff == 2.0
    assert relukko.max_delay is None
    assert relukko.exceptions == (requests.ConnectionError, RelukkoDoRetry)


def test_reconfigure_relukko_client_extended():
    relukko = RelukkoClient(base_url="", api_key="")

    cookies = cookiejar_from_dict({"cookie1": "value1"})
    verify_mode = ssl.VerifyMode.CERT_NONE
    verify_flags = ssl.VerifyFlags.VERIFY_DEFAULT
    options = ssl.Options.OP_ALL

    relukko.reconfigure_relukko(
        base_url="https://relukko", api_key="my-API-key", trust_env=False,
        cookies=cookies, total=100, connect=99, read=98, redirect=97,
        status=96, other=95, backoff_factor=94, backoff_max=1000,
        backoff_jitter=93, raise_on_redirect=False, raise_on_status=False,
        check_hostname=False, hostname_checks_common_name=False,
        verify_mode=verify_mode, verify_flags=verify_flags, options=options
    )

    assert relukko.session.trust_env == False
    assert relukko.session.cookies == cookies
    assert relukko.base_url == parse_url("https://relukko")
    assert relukko.ws_url == parse_url("wss://relukko/ws/broadcast")
    assert relukko.session.headers['X-api-Key'] == "my-API-key"
    http_adapter: HTTPAdapter = relukko.session.adapters.get("http://")
    assert http_adapter.max_retries.total == 100
    assert http_adapter.max_retries.connect == 99
    assert http_adapter.max_retries.read == 98
    assert http_adapter.max_retries.redirect == 97
    assert http_adapter.max_retries.status == 96
    assert http_adapter.max_retries.other == 95
    assert http_adapter.max_retries.backoff_factor == 94.0
    assert http_adapter.max_retries.backoff_max == 1000.0
    assert http_adapter.max_retries.backoff_jitter == 93.0
    assert http_adapter.max_retries.raise_on_redirect == False
    assert http_adapter.max_retries.raise_on_status == False
    assert isinstance(relukko.ssl_ctx, ssl.SSLContext)
    assert relukko.ssl_ctx.check_hostname == False
    assert relukko.ssl_ctx.hostname_checks_common_name == False
    assert relukko.ssl_ctx.keylog_filename is None
    assert relukko.ssl_ctx.sni_callback is None
    assert relukko.ssl_ctx.verify_flags == verify_flags
    assert relukko.ssl_ctx.verify_mode == verify_mode
    assert relukko.ssl_ctx.options == options
    assert relukko.tries == 4
    assert relukko.delay == 5
    assert relukko.backoff == 2.0
    assert relukko.max_delay is None
    assert relukko.exceptions == (requests.ConnectionError, RelukkoDoRetry)


def test_init_relukko_client_extented():
    cookies = cookiejar_from_dict({"cookie1": "value1"})

    relukko = RelukkoClient(
        base_url="http://relukko", api_key="my-API-key", trust_env=False,
        cookies=cookies, total=100, connect=99, read=98, redirect=97,
        status=96, other=95, backoff_factor=94, backoff_max=1000,
        backoff_jitter=93, raise_on_redirect=False, raise_on_status=False,
    )

    assert relukko.session.trust_env == False
    assert relukko.session.cookies == cookies
    assert relukko.base_url == parse_url("http://relukko")
    assert relukko.ws_url == parse_url("ws://relukko/ws/broadcast")
    assert relukko.session.headers['X-api-Key'] == "my-API-key"
    assert relukko.ssl_ctx is None
    http_adapter: HTTPAdapter = relukko.session.adapters.get("http://")
    assert http_adapter.max_retries.total == 100
    assert http_adapter.max_retries.connect == 99
    assert http_adapter.max_retries.read == 98
    assert http_adapter.max_retries.redirect == 97
    assert http_adapter.max_retries.status == 96
    assert http_adapter.max_retries.other == 95
    assert http_adapter.max_retries.backoff_factor == 94.0
    assert http_adapter.max_retries.backoff_max == 1000.0
    assert http_adapter.max_retries.backoff_jitter == 93.0
    assert http_adapter.max_retries.raise_on_redirect == False
    assert http_adapter.max_retries.raise_on_status == False
    assert relukko.tries == 4
    assert relukko.delay == 5
    assert relukko.backoff == 2.0
    assert relukko.max_delay is None
    assert relukko.exceptions == (requests.ConnectionError, RelukkoDoRetry)


def test_init_relukko_client_ssl_ctx():
    relukko = RelukkoClient(
        base_url="https://relukko", api_key="my-API-key",
    )

    default_ctx = ssl.create_default_context()

    assert isinstance(relukko.ssl_ctx, ssl.SSLContext)
    assert relukko.ssl_ctx.check_hostname == default_ctx.check_hostname
    assert relukko.ssl_ctx.hostname_checks_common_name == \
        default_ctx.hostname_checks_common_name
    assert relukko.ssl_ctx.keylog_filename == default_ctx.keylog_filename
    assert relukko.ssl_ctx.sni_callback == default_ctx.sni_callback
    assert relukko.ssl_ctx.verify_flags == default_ctx.verify_flags
    assert relukko.ssl_ctx.verify_mode == default_ctx.verify_mode
    assert relukko.ssl_ctx.options == default_ctx.options
    assert relukko.tries == 4
    assert relukko.delay == 5
    assert relukko.backoff == 2.0
    assert relukko.max_delay is None
    assert relukko.exceptions == (requests.ConnectionError, RelukkoDoRetry)


def test_init_relukko_client_ssl_ctx_extended():
    verify_mode = ssl.VerifyMode.CERT_NONE
    verify_flags = ssl.VerifyFlags.VERIFY_DEFAULT
    options = ssl.Options.OP_ALL

    relukko = RelukkoClient(
        base_url="https://relukko", api_key="my-API-key", check_hostname=False,
        hostname_checks_common_name=False, verify_mode=verify_mode,
        verify_flags=verify_flags, options=options
    )
    assert isinstance(relukko.ssl_ctx, ssl.SSLContext)
    assert relukko.ssl_ctx.check_hostname == False
    assert relukko.ssl_ctx.hostname_checks_common_name == False
    assert relukko.ssl_ctx.keylog_filename is None
    assert relukko.ssl_ctx.sni_callback is None
    assert relukko.ssl_ctx.verify_flags == verify_flags
    assert relukko.ssl_ctx.verify_mode == verify_mode
    assert relukko.ssl_ctx.options == options
    assert relukko.tries == 4
    assert relukko.delay == 5
    assert relukko.backoff == 2.0
    assert relukko.max_delay is None
    assert relukko.exceptions == (requests.ConnectionError, RelukkoDoRetry)


def test_init_relukko_client_retry():

    exceptions = (
        ValueError,
        ConnectionRefusedError,
        ConnectionResetError,
    )

    relukko = RelukkoClient(
        base_url="", api_key="", tries=100, delay=99, backoff=98.7,
        max_delay=97, exceptions=exceptions)

    assert relukko.tries == 100
    assert relukko.delay == 99
    assert relukko.backoff == 98.7
    assert relukko.max_delay == 97
    assert relukko.exceptions == exceptions


def test_reconfigre_relukko_client_retry():
    relukko = RelukkoClient(base_url="", api_key="")

    exceptions = (
        ValueError,
        ConnectionRefusedError,
        ConnectionResetError,
    )

    relukko.reconfigure_relukko(
        tries=100, delay=99, backoff=98.7,
        max_delay=97, exceptions=exceptions)

    assert relukko.tries == 100
    assert relukko.delay == 99
    assert relukko.backoff == 98.7
    assert relukko.max_delay == 97
    assert relukko.exceptions == exceptions


def test_acquire_relukko(relukko_backend):
    relukko, _ = relukko_backend

    lock = relukko.acquire_relukko("pylock", "pytest", 10)
    assert isinstance(lock, Dict)
    start_time = time.time()
    lock = relukko.acquire_relukko("pylock", "pytest", 30)
    end_time = time.time()
    assert lock is None
    assert 29 < end_time - start_time < 34


def test_delete_relukko(relukko_backend):
    relukko, _ = relukko_backend

    lock = relukko.acquire_relukko("del_lock", "pytest", 10)
    del_lock = relukko.delete_relukko(lock['id'])
    assert lock == del_lock

    get_lock = relukko.get_lock(lock['id'])
    assert get_lock['status'] == 422

    locks = relukko.get_locks()
    assert lock not in locks


def test_get_relukko(relukko_backend):
    relukko, _ = relukko_backend

    lock = relukko.acquire_relukko("get_lock", "pytest", 10)
    assert isinstance(lock, Dict)

    get_lock = relukko.get_lock(lock['id'])
    assert get_lock == lock

    locks = relukko.get_locks()
    assert lock in locks


def test_update_relukko_creator(relukko_backend):
    relukko, _ = relukko_backend

    lock = relukko.acquire_relukko("update_lock", "pytest", 10)
    assert lock['creator'] == "pytest"

    upd_lock = relukko.update_relukko(lock['id'], creator="tsetyp")
    assert upd_lock['creator'] == "tsetyp"

    get_lock = relukko.get_lock(lock['id'])
    assert get_lock['creator'] == "tsetyp"


def test_update_relukko_expires_at(relukko_backend):
    relukko, _ = relukko_backend

    lock = relukko.acquire_relukko("update_lock_exp", "pytest", 10)
    id = lock['id']

    expires_at = datetime.fromisoformat("2099-12-31T12:34:56.789Z")
    upd_lock = relukko.update_relukko(id, expires_at=expires_at)
    assert upd_lock['expires_at'] == "2099-12-31T12:34:56.789Z"

    with pytest.raises(ValueError):
        relukko.update_relukko(id, expires_at="2099-12-01T23:12Z")


def test_keep_relukko_alive(relukko_backend):
    relukko, _ = relukko_backend

    lock = relukko.acquire_relukko("keep_me", "pytest", 10)
    id = lock['id']

    start_time = datetime.now(timezone.utc)
    keep_lock = relukko.keep_relukko_alive(id)
    assert lock['expires_at'] is not keep_lock['expires_at']

    expires_at = datetime.fromisoformat(keep_lock['expires_at'])

    expires_diff = expires_at - start_time
    assert 295 < expires_diff.seconds < 305


def test_keep_relukko_alive_put(relukko_backend):
    relukko, _ = relukko_backend

    lock = relukko.acquire_relukko("keep_me_more", "pytest", 10)
    id = lock['id']

    start_time = datetime.now(timezone.utc)
    keep_lock = relukko.keep_relukko_alive_put(id, 3600)
    assert lock['expires_at'] is not keep_lock['expires_at']

    expires_at = datetime.fromisoformat(keep_lock['expires_at'])

    expires_diff = expires_at - start_time
    assert 3595 < expires_diff.seconds < 3605


def test_add_to_expires_at_time(relukko_backend):
    relukko, _ = relukko_backend
    lock = relukko.acquire_relukko("add_me_up", "pytest", 10)
    id = lock['id']
    start_expires_at = datetime.fromisoformat(lock['expires_at'])

    add_lock = relukko.add_to_expires_at_time(id)
    assert lock['expires_at'] is not add_lock['expires_at']

    add_expires_at = datetime.fromisoformat(add_lock['expires_at'])

    expires_diff = add_expires_at - start_expires_at
    assert expires_diff.seconds == 300


def test_add_to_expires_at_time_put(relukko_backend):
    relukko, _ = relukko_backend
    lock = relukko.acquire_relukko("add_me_up_more", "pytest", 10)
    id = lock['id']
    start_expires_at = datetime.fromisoformat(lock['expires_at'])

    add_lock = relukko.add_to_expires_at_time_put(id, 3600)
    assert lock['expires_at'] is not add_lock['expires_at']

    add_expires_at = datetime.fromisoformat(add_lock['expires_at'])

    expires_diff = add_expires_at - start_expires_at
    assert expires_diff.seconds == 3600
