import re
from typing import TYPE_CHECKING, Annotated, cast

import pytest
from fastapi import APIRouter, BackgroundTasks, Depends, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel

from cadwyn import Cadwyn
from cadwyn.exceptions import CadwynStructureError
from cadwyn.route_generation import VersionedAPIRouter
from cadwyn.structure.endpoints import endpoint
from cadwyn.structure.schemas import schema
from cadwyn.structure.versions import HeadVersion, Version, VersionBundle, VersionChange
from tests._resources.utils import BASIC_HEADERS, DEFAULT_API_VERSION
from tests._resources.versioned_app.app import (
    client_without_headers,
    client_without_headers_and_with_custom_api_version_var,
    v2021_01_01_router,
    v2022_01_02_router,
)

if TYPE_CHECKING:
    from fastapi.routing import APIRoute


def test__header_routing__invalid_version_format__error():
    main_app = Cadwyn(versions=VersionBundle(Version("2022-11-16")))
    with pytest.warns(DeprecationWarning):
        main_app.add_header_versioned_routers(  # pyright: ignore[reportDeprecated]
            APIRouter(),
            header_value=DEFAULT_API_VERSION,
        )
        with pytest.raises(ValueError, match=re.escape("header_value should be in ISO 8601 format")):
            main_app.add_header_versioned_routers(  # pyright: ignore[reportDeprecated]
                APIRouter(),
                header_value="2022-01_01",
            )


def test__header_routing_fastapi_init__openapi_passing_nulls__should_not_add_openapi_routes():
    assert [cast("APIRoute", r).path for r in Cadwyn(versions=VersionBundle(Version("2022-11-16"))).routes] == [
        "/docs/oauth2-redirect",
        "/changelog",
        "/openapi.json",
        "/docs",
        "/redoc",
    ]
    assert [
        cast("APIRoute", r).path
        for r in Cadwyn(versions=VersionBundle(Version("2022-11-16")), docs_url=None, redoc_url=None).routes
    ] == [
        "/changelog",
        "/openapi.json",
    ]
    assert Cadwyn(versions=VersionBundle(Version("2022-11-16")), openapi_url=None, changelog_url=None).routes == []


def test__header_routing_fastapi_init__passing_null_to_oauth2__should_not_add_oauth2_redirect_route():
    app = Cadwyn(versions=VersionBundle(Version("2022-11-16")), swagger_ui_oauth2_redirect_url=None)
    assert [cast("APIRoute", r).path for r in app.routes] == [
        "/changelog",
        "/openapi.json",
        "/docs",
        "/redoc",
    ]
    with pytest.warns(DeprecationWarning):
        app.add_header_versioned_routers(  # pyright: ignore[reportDeprecated]
            v2021_01_01_router, header_value="2021-01-01"
        )

    with TestClient(app) as client:
        assert client.get("/docs?version=2021-01-01").status_code == 200


def test__header_routing_fastapi_init__changing_openapi_url__docs_still_return_200():
    app = Cadwyn(versions=VersionBundle(Version("2022-11-16")), openapi_url="/openpapi")
    with pytest.warns(DeprecationWarning):
        app.add_header_versioned_routers(  # pyright: ignore[reportDeprecated]
            v2021_01_01_router, header_value="2021-01-01"
        )
        app.add_header_versioned_routers(  # pyright: ignore[reportDeprecated]
            v2022_01_02_router, header_value="2022-02-02"
        )
    with TestClient(app) as client:
        assert client.get("/openpapi?version=2021-01-01").status_code == 200
        assert client.get("/openapi.json?version=2021-01-01").status_code == 404


def test__header_routing_fastapi__calling_openapi_incorrectly__docs_should_return_404():
    app = Cadwyn(changelog_url=None, versions=VersionBundle(Version("2022-11-16")))
    with pytest.warns(DeprecationWarning):
        app.add_header_versioned_routers(  # pyright: ignore[reportDeprecated]
            v2021_01_01_router,
            header_value="2021-01-01",
        )
        app.add_header_versioned_routers(  # pyright: ignore[reportDeprecated]
            v2022_01_02_router,
            header_value="2022-02-02",
        )
    with TestClient(app) as client:
        assert client.get("/openapi.json?version=2021-01-01").status_code == 200
        # - Nonexisting version
        assert client.get("/openapi.json?version=2019-01-01").status_code == 404
        # - Nonexisting but compatible version
        assert client.get("/openapi.json?version=2024-01-01").status_code == 404
        # - version = null
        assert client.get("/openapi.json?version=").status_code == 404
        # - version not passed at all
        assert client.get("/openapi.json").status_code == 404
        # - Unversioned when we haven't added any
        assert client.get("/openapi.json?version=unversioned").status_code == 404

        @app.post("/my_unversioned_route")
        def my_unversioned_route():
            raise NotImplementedError

        assert client.get("/openapi.json?version=unversioned").status_code == 200


def test__cadwyn__with_dependency_overrides__overrides_should_be_applied():
    app = Cadwyn(versions=VersionBundle(Version("2022-11-16")))

    async def old_dependency():
        raise NotImplementedError

    async def new_dependency():
        return "new"

    @app.post("/hello")
    async def hello(dependency: Annotated[str, Depends(old_dependency)]):
        return dependency

    regular_router = APIRouter()

    @regular_router.post("/darkness")
    async def darkness(dependency: Annotated[str, Depends(old_dependency)]):
        return dependency

    with pytest.warns(DeprecationWarning):
        app.add_header_versioned_routers(  # pyright: ignore[reportDeprecated]
            regular_router,
            header_value="2022-11-16",
        )

    versioned_router = VersionedAPIRouter()

    @versioned_router.post("/my_old_friend")
    async def my_old_friend(dependency: Annotated[str, Depends(old_dependency)]):
        return dependency

    app.generate_and_include_versioned_routers(versioned_router)

    app.dependency_overrides = {old_dependency: new_dependency}
    with TestClient(app) as client:
        assert client.post("/hello").json() == "new"
        assert client.post("/darkness", headers={"x-api-version": "2022-11-16"}).json() == "new"
        assert client.post("/my_old_friend", headers={"x-api-version": "2022-11-16"}).json() == "new"


def test__header_routing_fastapi_add_header_versioned_routers__apirouter_is_empty__version_should_not_have_any_routes():
    app = Cadwyn(versions=VersionBundle(Version("2022-11-16")))
    with pytest.warns(DeprecationWarning):
        app.add_header_versioned_routers(  # pyright: ignore[reportDeprecated]
            APIRouter(),
            header_value="2022-11-16",
        )
    assert len(app.router.versioned_routers) == 1
    assert len(app.router.versioned_routers["2022-11-16"].routes) == 1
    route = cast("APIRoute", app.router.versioned_routers["2022-11-16"].routes[0])
    assert route.path == "/openapi.json"


@pytest.mark.parametrize("client", [client_without_headers, client_without_headers_and_with_custom_api_version_var])
def test__header_based_versioning(client: TestClient):
    resp = client.get("/v1", headers=BASIC_HEADERS)
    assert resp.status_code == 200
    assert resp.json() == {"my_version1": 1}
    assert resp.headers["X-API-VERSION"] == "2021-01-01"

    resp = client.get("/v1", headers=BASIC_HEADERS | {"X-API-VERSION": "2022-02-02"})
    assert resp.status_code == 200
    assert resp.json() == {"my_version2": 2}
    assert resp.headers["X-API-VERSION"] == "2022-02-02"

    resp = client.get("/v1", headers=BASIC_HEADERS | {"X-API-VERSION": "2024-02-02"})
    assert resp.status_code == 200
    assert resp.json() == {"my_version2": 2}
    assert resp.headers["X-API-VERSION"] == "2024-02-02"


def test__header_based_versioning__invalid_version_header_format__should_raise_422():
    resp = client_without_headers.get("/v1", headers=BASIC_HEADERS | {"X-API-VERSION": "2022-02_02"})
    assert resp.status_code == 422
    assert resp.json() == {
        "detail": [
            {
                "type": "date_from_datetime_parsing",
                "loc": ["header", "x-api-version"],
                "msg": "Input should be a valid date or datetime, invalid date separator, expected `-`",
                "input": "2022-02_02",
                "ctx": {"error": "invalid date separator, expected `-`"},
            }
        ]
    }


def test__get_unversioned_router():
    resp = client_without_headers.post("/v1/unversioned")
    assert resp.status_code == 200
    assert resp.json() == {"saved": True}


def test__get_openapi():
    resp = client_without_headers.get("/openapi.json", headers={"x-api-version": "2021-01-01"})
    assert resp.status_code == 200

    resp = client_without_headers.get("/openapi.json?version=2021-01-01")
    assert resp.status_code == 200


def test__get_openapi__nonexisting_version__error():
    resp = client_without_headers.get("/openapi.json?version=2023-01-01")
    assert resp.status_code == 404
    assert resp.json() == {"detail": "OpenApi file of with version `2023-01-01` not found"}


def test__get_openapi__with_mounted_app__should_include_root_path_in_servers():
    root_app = FastAPI()
    root_app.mount("/my_api", Cadwyn(changelog_url=None, versions=VersionBundle(Version("2022-11-16"))))
    client = TestClient(root_app)

    resp = client.get("/my_api/openapi.json?version=2022-11-16")
    servers = resp.json()["servers"]
    assert "/my_api" in [server["url"] for server in servers]


def test__get_docs__without_unversioned_routes__should_return_all_versioned_doc_urls():
    app = Cadwyn(changelog_url=None, versions=VersionBundle(Version("2022-11-16")))
    with pytest.warns(DeprecationWarning):
        app.add_header_versioned_routers(  # pyright: ignore[reportDeprecated]
            v2021_01_01_router,
            header_value="2021-01-01",
        )
        app.add_header_versioned_routers(  # pyright: ignore[reportDeprecated]
            v2022_01_02_router,
            header_value="2022-02-02",
        )

    client = TestClient(app)

    resp = client.get("/docs")
    assert resp.status_code == 200
    assert "http://testserver/docs?version=2021-01-01" in resp.text
    assert "http://testserver/docs?version=2022-02-02" in resp.text
    assert "http://testserver/docs?version=unversioned" not in resp.text

    resp = client.get("/redoc")
    assert resp.status_code == 200
    assert "http://testserver/redoc?version=2021-01-01" in resp.text
    assert "http://testserver/redoc?version=2022-02-02" in resp.text
    assert "http://testserver/redoc?version=unversioned" not in resp.text


def test__get_docs__with_mounted_app__should_return_all_versioned_doc_urls():
    root_app = FastAPI()
    root_app.mount("/my_api", Cadwyn(changelog_url=None, versions=VersionBundle(Version("2022-11-16"))))
    client = TestClient(root_app)

    resp = client.get("/my_api/docs")
    assert "http://testserver/my_api/docs?version=2022-11-16" in resp.content.decode()


def test__get_docs__with_unversioned_routes__should_return_all_versioned_doc_urls():
    app = Cadwyn(versions=VersionBundle(Version("2022-11-16")))
    with pytest.warns(DeprecationWarning):
        app.add_header_versioned_routers(  # pyright: ignore[reportDeprecated]
            v2021_01_01_router,
            header_value="2021-01-01",
        )
        app.add_header_versioned_routers(  # pyright: ignore[reportDeprecated]
            v2022_01_02_router,
            header_value="2022-02-02",
        )

    @app.post("/my_unversioned_route")
    def my_unversioned_route():
        raise NotImplementedError

    client = TestClient(app)

    resp = client.get("/docs")
    assert resp.status_code == 200
    assert "http://testserver/docs?version=2022-02-02" in resp.text
    assert "http://testserver/docs?version=2021-01-01" in resp.text
    assert "http://testserver/docs?version=unversioned" in resp.text

    resp = client.get("/redoc")
    assert resp.status_code == 200
    assert "http://testserver/redoc?version=2022-02-02" in resp.text
    assert "http://testserver/redoc?version=2021-01-01" in resp.text
    assert "http://testserver/redoc?version=unversioned" in resp.text


# I wish we could check it properly but it's a dynamic page and I'm not in the mood of adding selenium
def test__get_docs__specific_version():
    resp = client_without_headers.get("/docs?version=2022-01-01")
    assert resp.status_code == 200

    resp = client_without_headers.get("/redoc?version=2022-01-01")
    assert resp.status_code == 200


def test__get_unversioned_with_redirect():
    resp = client_without_headers.post("/v1/unversioned/")
    assert resp.status_code == 200
    assert resp.json() == {"saved": True}


def test__get_unversioned_as_partial_because_of_method():
    resp = client_without_headers.patch("/v1/unversioned")
    assert resp.status_code == 405


def test__empty_root():
    resp = client_without_headers.get("/")
    assert resp.status_code == 404


def test__background_tasks():
    background_task_data = None

    def my_background_task(email: str, message: str):
        nonlocal background_task_data
        background_task_data = (email, message)

    router = VersionedAPIRouter()

    @router.post("/send-notification/{email}")
    async def send_notification(email: str, background_tasks: BackgroundTasks):
        background_tasks.add_task(my_background_task, email, message="some notification")
        return {"message": "Notification sent in the background"}

    app = Cadwyn(versions=VersionBundle(HeadVersion(), Version(DEFAULT_API_VERSION)))
    app.generate_and_include_versioned_routers(router)

    with TestClient(app) as client:
        resp = client.post("/send-notification/test@example.com", headers=BASIC_HEADERS)
        assert resp.status_code == 200, resp.json()
        assert background_task_data == ("test@example.com", "some notification")


def test__webhooks():
    webhooks = VersionedAPIRouter()

    class Subscription(BaseModel):
        username: str
        monthly_fee: float
        start_date: str

    @webhooks.post("new-subscription")
    def new_subscription(body: Subscription):  # pragma: no cover
        """We'll send you a POST request with this data upon new user subscription"""

    class MyVersionChange(VersionChange):
        description = "Mess with webhooks"
        instructions_to_migrate_to_previous_version = [
            endpoint("new-subscription", ["POST"]).didnt_exist,
            schema(Subscription).field("monthly_fee").didnt_exist,
        ]

    app = Cadwyn(
        versions=VersionBundle(HeadVersion(), Version("2023-04-12", MyVersionChange), Version("2022-11-16")),
        webhooks=webhooks,
    )

    @app.webhooks.post("post-subscription")  # pragma: no cover
    def post_subscription(body: Subscription):  # pragma: no cover
        """I should also appear there"""

    with TestClient(app) as client:
        resp = client.get("/openapi.json?version=2023-04-12")
        openapi_dict = resp.json()

        assert "webhooks" in openapi_dict, "'webhooks' section is missing"
        assert "new-subscription" in openapi_dict["webhooks"], "'new-subscription' webhook is missing"
        assert "post-subscription" in openapi_dict["webhooks"], "'post-subscription' webhook is missing"
        assert "post" in openapi_dict["webhooks"]["post-subscription"], "POST method for 'post-subscription' is missing"
        assert "Subscription" in openapi_dict["components"]["schemas"], "'Subscription' component is missing"
        assert "monthly_fee" in openapi_dict["components"]["schemas"]["Subscription"]["properties"], (
            "monthly_fee field is missing"
        )

        resp = client.get("/openapi.json?version=2022-11-16")
        openapi_dict = resp.json()

        assert "webhooks" in openapi_dict, "'webhooks' section is missing"
        assert "new-subscription" not in openapi_dict["webhooks"], "'new-subscription' webhook is missing"
        assert "post-subscription" in openapi_dict["webhooks"], "'post-subscription' webhook is present"
        assert "post" in openapi_dict["webhooks"]["post-subscription"], "POST method for 'post-subscription' is missing"
        assert "Subscription" in openapi_dict["components"]["schemas"], "'Subscription' component is missing"
        assert "monthly_fee" not in openapi_dict["components"]["schemas"]["Subscription"]["properties"], (
            "monthly_fee field is present yet it must be deleted"
        )


def test__docs_dashboards__custom_static_asset_urls():
    app = Cadwyn(
        versions=VersionBundle(Version("2022-11-16")),
        changelog_url=None,
        swagger_js_url="/static/swagger.js",
        swagger_css_url="/static/swagger.css",
        swagger_favicon_url="/static/swagger-favicon.png",
        redoc_js_url="/static/redoc.js",
        redoc_favicon_url="/static/redoc-favicon.png",
    )
    with TestClient(app) as client:
        swagger_resp = client.get("/docs?version=2022-11-16")
        assert swagger_resp.status_code == 200
        assert "/static/swagger.js" in swagger_resp.text
        assert "/static/swagger.css" in swagger_resp.text
        assert "/static/swagger-favicon.png" in swagger_resp.text

        redoc_resp = client.get("/redoc?version=2022-11-16")
        assert redoc_resp.status_code == 200
        assert "/static/redoc.js" in redoc_resp.text
        assert "/static/redoc-favicon.png" in redoc_resp.text


def test__api_version_header_name_is_deprecated_and_translates_to_api_version_parameter_name():
    with pytest.warns(DeprecationWarning):
        cadwyn = Cadwyn(api_version_header_name="x-api-version", versions=VersionBundle(Version("2022-11-16")))
    assert cadwyn.api_version_parameter_name == "x-api-version"


def test__api_version_default_value_with_path_location__should_raise_error():
    with pytest.raises(
        CadwynStructureError,
        match="You tried to pass an api_version_default_value while putting the API version in Path",
    ):
        Cadwyn(
            versions=VersionBundle(HeadVersion(), Version("2022-11-16")),
            api_version_default_value="2022-11-16",
            api_version_location="path",
        )
