from __future__ import annotations

from dataclasses import dataclass
from typing import Iterable
from uuid import UUID

import pytest
from django.contrib.auth import get_user_model
from django.test import Client
from django.urls import URLPattern, URLResolver, get_resolver, reverse


SAFE_STATUS_CODES = {
    200,
    201,
    202,
    204,
    301,
    302,
    303,
    304,
    307,
    308,
    400,
    401,
    403,
    404,
    405,
}

SKIP_NAMES = {
    "admin:index",  # admin app has its own dedicated tests and redirects heavily.
}

SKIP_NAMESPACES = {
    "admin",
    "tinymce",
    "cookies",
}

STANDARD_NAME_HINTS = {
    "index",
    "home",
    "help",
    "about",
    "privacy",
    "stats",
    "list",
    "detail",
    "view",
    "overview",
    "profile",
    "inbox",
    "messages",
    "people",
    "server",
    "logs",
    "trainees",
    "normals",
    "categories",
    "search_page",
}

NON_STANDARD_NAME_HINTS = {
    "create",
    "update",
    "delete",
    "remove",
    "add",
    "edit",
    "toggle",
    "submit",
    "save",
    "bulk",
    "json",
    "dicom",
    "viewer",
    "download",
    "upload",
    "anonymise",
    "migrate",
    "truncate",
    "image_size",
    "series",
    "mark",
    "review",
    "sync",
    "reset",
    "api",
}


@dataclass(frozen=True)
class NamedRoute:
    name: str
    kwargs: dict[str, object]


def _default_for_converter(converter_name: str) -> object:
    if converter_name == "int":
        return 1
    if converter_name == "str":
        return "smoke"
    if converter_name == "slug":
        return "smoke"
    if converter_name == "uuid":
        return UUID("00000000-0000-0000-0000-000000000001")
    if converter_name == "path":
        return "smoke/path"
    return "smoke"


def _iter_named_patterns(
    patterns: Iterable[URLPattern | URLResolver],
    namespace: str = "",
) -> Iterable[NamedRoute]:
    for pattern in patterns:
        if isinstance(pattern, URLResolver):
            nested_namespace = namespace
            if pattern.namespace:
                nested_namespace = f"{namespace}:{pattern.namespace}" if namespace else pattern.namespace
            yield from _iter_named_patterns(pattern.url_patterns, nested_namespace)
            continue

        if not pattern.name:
            continue

        full_name = f"{namespace}:{pattern.name}" if namespace else pattern.name
        converters = getattr(pattern.pattern, "converters", {})
        kwargs = {key: _default_for_converter(converter.__class__.__name__.replace("Converter", "").lower()) for key, converter in converters.items()}
        yield NamedRoute(name=full_name, kwargs=kwargs)


def _all_named_routes() -> list[NamedRoute]:
    resolver = get_resolver()
    routes = list(_iter_named_patterns(resolver.url_patterns))

    unique_routes: dict[tuple[str, tuple[tuple[str, object], ...]], NamedRoute] = {}
    for route in routes:
        key = (route.name, tuple(sorted(route.kwargs.items())))
        unique_routes[key] = route

    return sorted(unique_routes.values(), key=lambda route: (route.name, sorted(route.kwargs)))


def _is_standard_route(route_name: str) -> bool:
    parts = route_name.split(":")
    namespace = parts[0] if len(parts) > 1 else ""
    leaf_name = parts[-1]

    if namespace in SKIP_NAMESPACES:
        return False
    if route_name in SKIP_NAMES:
        return False
    if any(hint in leaf_name for hint in NON_STANDARD_NAME_HINTS):
        return False

    return any(hint in leaf_name for hint in STANDARD_NAME_HINTS)


ALL_NAMED_ROUTES = _all_named_routes()
STANDARD_NAMED_ROUTES = [route for route in ALL_NAMED_ROUTES if _is_standard_route(route.name)]


@pytest.fixture
def smoke_client(db) -> Client:
    user_model = get_user_model()
    user = user_model.objects.create_user(
        username="view_smoke_user",
        password="testpassword",
        email="view_smoke_user@example.com",
    )
    client = Client()
    client.raise_request_exception = False
    client.force_login(user)
    return client


@pytest.mark.django_db
@pytest.mark.parametrize("route", STANDARD_NAMED_ROUTES, ids=lambda route: route.name)
def test_named_view_routes_do_not_return_server_errors(smoke_client: Client, route: NamedRoute):
    if route.name in SKIP_NAMES:
        pytest.skip("Route is intentionally excluded from this smoke suite.")

    try:
        url = reverse(route.name, kwargs=route.kwargs or None)
    except Exception as exc:  # pragma: no cover
        pytest.skip(f"Unable to reverse route {route.name}: {exc}")

    response = smoke_client.get(url, follow=False)

    assert response.status_code in SAFE_STATUS_CODES, (
        f"Unexpected status for {route.name} ({url}): {response.status_code}"
    )


@pytest.mark.django_db
@pytest.mark.parametrize("route", STANDARD_NAMED_ROUTES, ids=lambda route: route.name)
def test_named_view_routes_anonymous_do_not_return_server_errors(route: NamedRoute):
    if route.name in SKIP_NAMES:
        pytest.skip("Route is intentionally excluded from this smoke suite.")

    try:
        url = reverse(route.name, kwargs=route.kwargs or None)
    except Exception as exc:  # pragma: no cover
        pytest.skip(f"Unable to reverse route {route.name}: {exc}")

    client = Client()
    client.raise_request_exception = False
    response = client.get(url, follow=False)

    assert response.status_code in SAFE_STATUS_CODES, (
        f"Unexpected anonymous status for {route.name} ({url}): {response.status_code}"
    )
