import hashlib
import itertools
import platform
import uuid
from typing import Any, Dict, List, Optional

import pytest
from pytest_httpserver import HTTPServer
from werkzeug import Request, Response

from svix import __version__
from svix.api import (
    ApplicationIn,
    ApplicationOut,
    EndpointIn,
    EndpointOut,
    EventTypeIn,
    EventTypeOut,
    MessageIn,
    Svix,
)
from svix.webhooks import Webhook


def _gen_uid(name: str) -> str:
    hasher = hashlib.sha256()
    hasher.update(name.encode("utf-8"))
    hasher.update(str(uuid.uuid4()).encode("ascii"))
    return hasher.hexdigest()


@pytest.fixture
def svix_app_name() -> str:
    return "svix_python_tests"


def event_type_name() -> str:
    return f"event.{str(uuid.uuid4()).replace('-', '')}"


def make_event_type_schema(event_type_name) -> Dict[str, Any]:
    return {
        "type": "object",
        "title": event_type_name,
        "description": "A dummy event type",
        "properties": {
            "value": {
                "type": "string",
                "description": "A simple string value",
            }
        },
        "required": ["value"],
    }


@pytest.fixture
def endpoint_url() -> str:
    return "http://localhost/webhook/receiver"


def create_svix_app(
    svix_api: Svix, svix_app_name: str, svix_app_uid: str
) -> ApplicationOut:
    return svix_api.application.get_or_create(
        ApplicationIn(name=svix_app_name, uid=svix_app_uid)
    )


def create_svix_event_type(
    svix_api: Svix, event_type_schema: Dict[str, Any]
) -> EventTypeOut:
    return svix_api.event_type.create(
        EventTypeIn(
            name=event_type_schema["title"],
            description=event_type_schema["description"],
            schemas={"1": event_type_schema},
        )
    )


def create_svix_endpoint(
    svix_api: Svix,
    app_uid: str,
    event_type_name: str,
    endpoint_url: str,
    endpoint_uid: str,
    channel: Optional[str] = None,
    metadata: Optional[Dict[str, Any]] = None,
    secret: Optional[str] = None,
) -> EndpointOut:
    return svix_api.endpoint.create(
        app_uid,
        EndpointIn(
            url=endpoint_url,
            uid=endpoint_uid,
            event_types=[event_type_name],
            channels=[channel] if channel else None,
            metadata=metadata,
            secret=secret,
        ),
    )


def test_svix_application_create(svix_api: Svix, svix_app_name: str) -> None:
    svix_app_uid = _gen_uid(svix_app_name)
    app = create_svix_app(svix_api, svix_app_name, svix_app_uid)
    assert app.name == svix_app_name
    assert app.uid == svix_app_uid


def test_svix_event_type_create(svix_api: Svix) -> None:
    event_type_schema = make_event_type_schema(event_type_name())
    event_type = create_svix_event_type(svix_api, event_type_schema)
    assert event_type.name == event_type_schema["title"]
    assert event_type.description == event_type_schema["description"]
    assert event_type.schemas == {"1": event_type_schema}


def svix_endpoint_create_test_params_ids() -> List[str]:
    ids = []
    for params in itertools.product([False, True], repeat=3):
        with_channel, with_metadata, with_secret = params
        ids.append(
            "/".join(
                [
                    ("with" if with_channel else "without") + " channel",
                    ("with" if with_metadata else "without") + " metadata",
                    ("with" if with_secret else "without") + " secret",
                ]
            )
        )
    return ids


def test_svix_basic_headers(httpx_mock):
    svix_api = Svix(auth_token="not-a-real-auth-token")
    httpx_mock.add_response(
        url="https://api.svix.com/api/v1/app",
        json={
            "data": [],
            "done": True,
            "iterator": "app_3EXsi4X5ClkrXn5GqkzvpnNvwqj",
            "prevIterator": "-app_3EXsi4X5ClkrXn5GqkzvpnNvwqj",
        },
    )
    svix_api.application.list()
    req = httpx_mock.get_request()
    assert req.headers["authorization"] is not None
    assert req.headers["svix-req-id"] is not None
    assert req.headers["user-agent"] == " ".join(
        [
            f"svix-libs/{__version__}/python",
            f"python/{platform.python_version()}",
            f"{platform.system()}/{platform.machine()}",
        ]
    )


@pytest.mark.parametrize(
    "with_channel,with_metadata,with_secret",
    list(itertools.product([False, True], repeat=3)),
    ids=svix_endpoint_create_test_params_ids(),
)
def test_svix_endpoint_create(
    svix_api: Svix,
    svix_app_name: str,
    endpoint_url: str,
    with_channel: bool,
    with_metadata: bool,
    with_secret: bool,
) -> None:
    svix_app_uid = _gen_uid(svix_app_name)
    app = create_svix_app(svix_api, svix_app_name, svix_app_uid)
    event_type_schema = make_event_type_schema(event_type_name())
    event_type = create_svix_event_type(svix_api, event_type_schema)
    endpoint_uid = _gen_uid(endpoint_url)
    channel = "test" if with_channel else None
    metadata = {"test": "test"} if with_metadata else None
    secret = "whsec_" + "e" * 32 if with_secret else None
    assert app.uid
    endpoint = create_svix_endpoint(
        svix_api,
        app.uid,
        event_type.name,
        endpoint_url,
        endpoint_uid,
        channel,
        metadata,
        secret,
    )
    assert endpoint.url == endpoint_url
    assert endpoint.uid == endpoint_uid
    assert endpoint.event_types == [event_type.name]
    if with_channel:
        assert endpoint.channels == [channel]
    if with_metadata:
        assert endpoint.metadata == metadata
    if with_secret:
        assert svix_api.endpoint.get_secret(app.uid, endpoint_uid).key == secret


@pytest.mark.parametrize(
    "with_channel", [False, True], ids=["without channel", "with channel"]
)
def test_svix_message_create(
    svix_api: Svix,
    svix_app_name: str,
    httpserver: HTTPServer,
    with_channel: bool,
    is_ci: bool,
) -> None:
    svix_app_uid = _gen_uid(svix_app_name)
    create_svix_app(svix_api, svix_app_name, svix_app_uid)
    event_type_schema = make_event_type_schema(event_type_name())
    event_type = create_svix_event_type(svix_api, event_type_schema)

    channel = "test" if with_channel else None
    endpoint_path = "/webhook/receiver/"
    if is_ci:
        endpoint_url = httpserver.url_for(endpoint_path)
    else:
        endpoint_url = f"http://host.docker.internal:{httpserver.port}{endpoint_path}"
    endpoint_uid = _gen_uid(endpoint_url)
    create_svix_endpoint(
        svix_api,
        svix_app_uid,
        event_type.name,
        endpoint_url,
        endpoint_uid,
        channel=channel,
    )
    secret = svix_api.endpoint.get_secret(svix_app_uid, endpoint_uid).key

    payload = {"value": "test"}

    def webhook_handler(request: Request) -> Response:
        assert "Svix-Id" in request.headers
        assert "Svix-Timestamp" in request.headers
        assert "Svix-Signature" in request.headers

        webhook = Webhook(secret)
        headers: Dict[str, str] = dict(request.headers.items())
        webhook.verify(request.data, headers)

        return Response("OK")

    httpserver.expect_oneshot_request(
        endpoint_path,
        method="POST",
        json=payload,
    ).respond_with_handler(webhook_handler)

    # send message and check it is received by local http server
    with httpserver.wait(stop_on_nohandler=True, timeout=10) as waiting:
        message_out = svix_api.message.create(
            svix_app_uid,
            MessageIn(
                event_type=event_type.name,
                payload=payload,
                channels=[channel] if channel else None,
            ),
        )

    assert waiting.result

    httpserver.check()  # type: ignore[no-untyped-call]

    assert message_out.event_type == event_type.name
    assert message_out.event_type == event_type.name
    if with_channel:
        assert message_out.channels == [channel]
