import asyncio
import base64
import json
import tempfile
from collections.abc import AsyncGenerator, Generator
from datetime import UTC, datetime, timedelta
from typing import Any

import jwt
import pytest
import zmq
from asgi_lifespan import LifespanManager
from dynaconf import Dynaconf
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient
from pytest_httpx import HTTPXMock

from mrok.conf import Settings, get_settings
from mrok.types.proxy import ASGIReceive, ASGISend, Message
from tests.types import ReceiveFactory, SendFactory, SettingsFactory, StatusEventFactory


@pytest.fixture(scope="session")
def settings_factory() -> SettingsFactory:
    def _get_settings(
        ziti: dict | None = None,
        logging: dict | None = None,
        frontend: dict | None = None,
        controller: dict | None = None,
    ) -> Settings:
        ziti = ziti or {
            "base_urls": {
                "management": "https://ziti.example.com",
                "client": "https://ziti.example.com",
            },
            "connect_timeout": 0.25,
            "read_timeout": 10,
            "ssl_verify": True,
            "auth": {"username": "user", "password": "pass", "identity": None},
        }
        logging = logging or {
            "debug": True,
            "rich": False,
        }

        controller = controller or {}
        if "auth" not in controller:
            controller["auth"] = {
                "backends": ["oidc"],
                "oidc": {
                    "config_url": "http://example.com/openid-configuration",
                    "audience": "mrok-audience",
                    "subject_claim": "sub",
                },
            }
        if "pagination" not in controller:
            controller["pagination"] = {"limit": 5}
        frontend = frontend or {
            "identity": "public",
            "mode": "zrok",
            "domain": "exts.s1.today",
        }
        settings = Dynaconf(
            environments=True,
            settings_files=[],
            ENV_FOR_DYNACONF="testing",
            ZITI=ziti,
            LOGGING=logging,
            FRONTEND=frontend,
            CONTROLLER=controller,
        )

        return settings

    return _get_settings


@pytest.fixture()
def ziti_bad_request_error() -> dict[str, Any]:
    return {
        "error": {
            "code": "error_code",
            "message": "error_message",
            "cause": {
                "field": "field_name",
                "reason": "field_error",
            },
        }
    }


@pytest.fixture()
def ziti_identity_json() -> dict[str, Any]:
    return {
        "ztAPI": "https://ziti.platform.softwareone.com/edge/client/v1",
        "ztAPIs": None,
        "configTypes": None,
        "id": {
            "key": "pem:-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
            "cert": "pem:-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
            "ca": "pem:-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
        },
        "enableHa": False,
        "mrok": {
            "extension": "ext-0000-0000",
            "instance": "ins-0000-0000-0000",
            "domain": "exts.platform.softwareone.com",
            "tags": {
                "mrok-service": "ext-0000-0000",
                "mrok-identity-type": "instance",
                "mrok": "0.4.0",
            },
        },
    }


@pytest.fixture()
def ziti_identity_file(ziti_identity_json: dict[str, Any]) -> Generator[str, None, None]:
    with tempfile.NamedTemporaryFile("w", suffix="json") as f:
        json.dump(ziti_identity_json, f)
        f.seek(0)
        yield f.name


@pytest.fixture()
def jwt_signing_key() -> str:
    return """-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDh0IIuMVuxXSq1
ndmO13RLpqv+3Ndyh0aEP8Lz3/zQeyliYCfW8HMoP8vjC9ftGKdEHLG4eoUs6NF+
8dYh3MRtDikl0oH/J+/6DFS3zqG+NNaMUpTgmKsrn1w5MN/NPQxPp/Djc4nAcAoi
V/c0w5fft5CLoLkLt5iL8y2mTuR0VRhZ6fS+hQiQMcSBajtr8jPLhem9spJ53N5x
X/OQNTkP712s9qSwCLd8SoFzNFYam2kHKs+nfMbXmk0PXbLjTb9Sk6pOa4Ey5OZU
//8oFyMnEltuRUH158WskpxSsxwZosPOsrFXcEdq5DHPQppc5H6IjwevVr8VZCZ9
y0DhqbpHAgMBAAECggEAa23BiQ1VVIeA2p9qkbDBtH3qJJlW7DccHq2g51nf0iVN
1m3tdi21c2gKbQ1E4BjS9q36BMxREEEA7bZKy5hWYJWUkNhZnRSYb+qu4TUuMKm9
ikt9ZW5sPJgXjWPJjUAmW70HdoYQelszDwyvYExPNBEF9M91SXRCYA5EYPL2b7rp
FjUzbq4/a4C9GYUP7EhBYYyZ+KnFDa9mxdbUFrQgupV2b+v2sPeOmkDRhzjAp7HC
yRcUnTmQM32LMO57oFZ7K9DJDMwxbHCAE7N8f2Uge2VLpIUjz3GskdjPcLBqu4qH
938dL3GMvKglo18VRSKiTkwxjxnP8HA8+0ifRv7cIQKBgQD7irdN/lYXdlhKAXwt
eQ5Tua3/Zi3Xia0dgOEzGxfVT1L6rVjVa149/v7x26p0evt0c9gkcHlBgUDEH7yz
sEDY/KwIo9Wg2Bpe1iUNcO/BvZc0mc3Cu9D8agxNZ5eHq+a36gTKR/QD1dp+48zK
6yimXxEfUYy2u8TBLm2Jk3hx4QKBgQDl0RA+L/SP8d2lB4vPDSSFhMYnwaH4njbq
/S8evNFcyKba9HaA8Sbng/msI/k8rnyul5GqFVooVl9TfPa93ATvagzijBvvqZ6R
j2+41hEAxmZ2257DKYpQ4JM2mRl9pOFQrc1GRbUfSOJAekj2O/2ojnbHHUcxg0dS
yliskq6BJwKBgGxVDKcBb5CBPnr48sMezMXQRRimp/2Y5L69H8AD3hrXI/SkLYsU
x6zJooEFSv8JbDx2G9NtwTst8HfG910n/nW1NF4wOTQhfhH0BlcomYmGHpXf25cP
jmz3Oz8m60LaDO6OUevQW04/ju9xKmUGLCai8NvdIk4cxhsw5KoIoinhAoGBALDu
vCqkkQ0hkRs1LBZEcBG7nzOMiD740B8qvdRUWnusn4mDHJk5EFK98MLvDzwAuk1Q
s/zWY4satFl6pByX/9SzOShR5lAlrscyPzl21bBbDxgDDcADg1GxFKW8STvKbQ3I
QXoQwNlNK6OogfPRTAExbZDuoZklEQxUbOCwLVmRAoGACIi7DWWZr9Ea4ag82yYv
CBdlKSshb2RYGdbYm9dzeqqv1/xtwtFYMOuLilL3qp9erBAsc6Kdz+n3v1+2p7Ho
3/upcFI5QbhGHCO5J92fdDYh6rsAG4cwC5LlAC5WgP3bL9+QVKKcYm8/mmglLIu3
XIvgj23tDAmXtVRh0MsgOH4=
-----END PRIVATE KEY-----
"""


@pytest.fixture()
def jwt_token(jwt_signing_key: str) -> str:
    payload = {
        "sub": "user123",
        "aud": "mrok-audience",
        "iss": "http:/test.mrok/",
        "exp": datetime.now(UTC) + timedelta(minutes=30),
    }
    return jwt.encode(payload, jwt_signing_key, algorithm="RS256", headers={"kid": "test-key"})


@pytest.fixture()
def jwks_json() -> dict:
    return {
        "keys": [
            {
                "kty": "RSA",
                "use": "sig",
                "alg": "RS256",
                "kid": "test-key",
                "n": (
                    "4dCCLjFbsV0qtZ3Zjtd0S6ar_tzXcodGhD_C89_80HspYmAn1vBzKD_"
                    "L4wvX7RinRByxuHqFLOjRfvHWIdzEbQ4pJdKB_yfv-gxUt86hvjTWjF"
                    "KU4JirK59cOTDfzT0MT6fw43OJwHAKIlf3NMOX37eQi6C5C7eYi_Mtp"
                    "k7kdFUYWen0voUIkDHEgWo7a_Izy4XpvbKSedzecV_zkDU5D-9drPak"
                    "sAi3fEqBczRWGptpByrPp3zG15pND12y402_UpOqTmuBMuTmVP__KBc"
                    "jJxJbbkVB9efFrJKcUrMcGaLDzrKxV3BHauQxz0KaXOR-iI8Hr1a_FW"
                    "QmfctA4am6Rw"
                ),
                "e": "AQAB",
            }
        ]
    }


@pytest.fixture()
def openid_config() -> dict:
    return {"issuer": "http:/test.mrok/", "jwks_uri": "http://example.com/jwks.json"}


@pytest.fixture()
def fastapi_app(settings_factory: SettingsFactory) -> FastAPI:
    settings = settings_factory()
    from mrok.controller.app import setup_app

    app = setup_app(settings)
    app.dependency_overrides[get_settings] = lambda: settings
    return app


@pytest.fixture()
async def app_lifespan_manager(fastapi_app: FastAPI) -> AsyncGenerator[LifespanManager, None]:
    async with LifespanManager(fastapi_app) as lifespan_manager:
        yield lifespan_manager


@pytest.fixture
async def api_client(
    fastapi_app: FastAPI,
    app_lifespan_manager: LifespanManager,
    settings_factory: SettingsFactory,
    httpx_mock: HTTPXMock,
    openid_config: dict,
    jwks_json: dict,
    jwt_token: str,
) -> AsyncGenerator[AsyncClient]:
    settings = settings_factory()
    httpx_mock.add_response(
        method="GET",
        url=settings.controller.auth.oidc.config_url,
        json=openid_config,
        is_reusable=True,
    )
    httpx_mock.add_response(
        method="GET",
        url="http://example.com/jwks.json",
        json=jwks_json,
        is_reusable=True,
    )

    async with AsyncClient(
        transport=ASGITransport(app=app_lifespan_manager.app),
        base_url=f"http://localhost/{fastapi_app.root_path.removeprefix('/')}/",
        headers={"Authorization": f"Bearer {jwt_token}"},
    ) as client:
        yield client


@pytest.fixture
def receive_factory() -> ReceiveFactory:
    def _factory(messages: list[Message] | None = None) -> ASGIReceive:
        if not messages:
            messages = [{"type": "http.request", "body": b"", "more_body": False}]

        class Receiver:
            def __init__(self, messages: list[Message]):
                self.messages = messages

            async def __call__(self):
                await asyncio.sleep(0)
                try:
                    return self.messages.pop(0)
                except Exception:
                    return

        return Receiver(messages)

    return _factory


@pytest.fixture
def send_factory() -> SendFactory:
    def _factory(collected: list[Message]) -> ASGISend:
        async def send(message: Message) -> None:
            await asyncio.sleep(0)
            collected.append(message)

        return send

    return _factory


@pytest.fixture()
def response_event_factory():
    def _response_event(
        method: str = "GET",
        url: str = "/test",
        request_headers: dict[str, str] | None = None,
        request_querystring: bytes | None = None,
        request_body: bytes | None = None,
        request_truncated: bool = False,
        response_headers: dict[str, str] | None = None,
        response_status: int = 200,
        response_body: bytes | None = b'{"test": "json"}',
        response_truncated: bool = False,
        duration: float = 10.3,
    ) -> dict[str, Any]:
        request_headers = request_headers or {"accept": "application/json"}
        response_headers = response_headers or {"content-type": "application/json"}
        request = {
            "method": method,
            "url": url,
            "headers": request_headers,
            "query_string": None,
            "start_time": 0.0,
            "body": None,
            "body_truncated": request_truncated,
        }

        response = {
            "type": "response",
            "data": {
                "type": "response",
                "request": request,
                "status": response_status,
                "headers": response_headers,
                "duration": duration,
                "body": None,
                "body_truncated": response_truncated,
            },
        }
        if request_querystring:
            request["query_string"] = base64.b64encode(request_querystring).decode("ascii")
        if request_body:
            request["body"] = base64.b64encode(request_body).decode("ascii")
        if response_body:
            response["data"]["body"] = base64.b64encode(response_body).decode("ascii")  # type: ignore[index]
        return response

    return _response_event


@pytest.fixture()
def status_event_factory() -> StatusEventFactory:
    def _status_event(
        process_cpu: float = 55.1,
        process_mem: float = 21.2,
        requests_rps: int = 123,
        requests_total: int = 1000,
        requests_successful: int = 10,
        requests_failed: int = 30,
        bytes_in: int = 1000,
        bytes_out: int = 2000,
    ):
        return {
            "type": "status",
            "data": {
                "type": "status",
                "meta": {
                    "extension": "EXT-1223-4443",
                    "instance": "INS-3737-8373-7373-1113-3384",
                    "domain": "ext.mrok.test",
                    "tags": None,
                },
                "metrics": {
                    "worker_id": "my-worker-id",
                    "data_transfer": {"bytes_in": bytes_in, "bytes_out": bytes_out},
                    "requests": {
                        "rps": requests_rps,
                        "total": requests_total,
                        "successful": requests_successful,
                        "failed": requests_failed,
                    },
                    "response_time": {
                        "avg": 10.0,
                        "min": 1,
                        "max": 30,
                        "p50": 11,
                        "p90": 22,
                        "p99": 11,
                    },
                    "process": {"cpu": process_cpu, "mem": process_mem},
                },
            },
        }

    return _status_event


@pytest.fixture()
def multipart_body() -> tuple[str, bytes]:
    boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"
    data = b"""------WebKitFormBoundary7MA4YWxkTrZu0gW\r
Content-Disposition: form-data; name="username"\r
\r
john_doe\r
------WebKitFormBoundary7MA4YWxkTrZu0gW\r
Content-Disposition: form-data; name="email"\r
\r
john@example.com\r
------WebKitFormBoundary7MA4YWxkTrZu0gW\r
Content-Disposition: form-data; name="message"\r
\r
Hello, this is a test message!\r
------WebKitFormBoundary7MA4YWxkTrZu0gW\r
Content-Disposition: form-data; name="file"; filename="test.txt"\r
Content-Type: application/octet-stream\r
\r
binary file content\r
------WebKitFormBoundary7MA4YWxkTrZu0gW--\r
"""

    return boundary, data


@pytest.fixture(scope="session")
def zmq_publisher() -> Generator[tuple[zmq.Socket, int], None, None]:
    context: zmq.Context = zmq.Context.instance()
    socket: zmq.Socket = context.socket(zmq.XPUB)

    port: int = socket.bind_to_random_port("tcp://127.0.0.1")

    yield socket, port

    socket.close()
    context.term()


@pytest.fixture()
def ziti_frontend_error_template_html() -> str:
    return "{{ status }}\n{{ body }}\n"


@pytest.fixture()
def ziti_frontend_error_template_html_file(
    ziti_frontend_error_template_html: str,
) -> Generator[str, None, None]:
    with tempfile.NamedTemporaryFile("w", suffix="html") as f:
        f.write(ziti_frontend_error_template_html)
        f.seek(0)
        yield f.name


@pytest.fixture()
def ziti_frontend_error_template_json() -> str:
    return """{
        "status": {{ status }},
        "body": "{{ body }}",
    }
"""


@pytest.fixture()
def ziti_frontend_error_template_json_file(
    ziti_frontend_error_template_json: str,
) -> Generator[str, None, None]:
    with tempfile.NamedTemporaryFile("w", suffix="json") as f:
        f.write(ziti_frontend_error_template_json)
        f.seek(0)
        yield f.name
