# Copyright (c) Huawei Technologies Co., Ltd. 2026-2026. All rights reserved.
# OpenOLC is licensed under Mulan PSL v2.
# You can use this software according to the terms and conditions of the Mulan PSL v2.
# You may obtain a copy of Mulan PSL v2 at:
#         `http://license.coscl.org.cn/MulanPSL2`
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
# See the Mulan PSL v2 for more details.

"""FastAPI 适配器(ASGI 纯异步版本)的端到端测试。

使用 starlette TestClient + patch OLC.async_process,
不依赖真实 Redis 或 FastAPI 模板。
"""
import unittest
from unittest.mock import patch

from fastapi import FastAPI
from fastapi.testclient import TestClient

from olc.adapters.fastapi import OlcAdapterConfig, OlcFastAPIAdapter
from olc.bean.olc_control_result import OlcControlResult
from olc.control.context.context import OlcContext


def _make_app(allow: bool):
    """构造一个最小 FastAPI 应用,OLC.async_process 被 mock。"""
    app = FastAPI()

    @app.get("/ping")
    async def ping():
        return {"ok": True}

    async def fake_process(req):
        ctx = OlcContext(req)
        ctx.result = OlcControlResult()
        ctx.result.block = not allow
        return ctx

    async def fake_clean(ctx):
        return

    patcher_p = patch("olc.adapters.fastapi.adapter.OLC.async_process", side_effect=fake_process)
    patcher_c = patch("olc.adapters.fastapi.adapter.OLC.async_clean", side_effect=fake_clean)
    patcher_p.start()
    patcher_c.start()
    app.state._patchers = [patcher_p, patcher_c]

    app.add_middleware(OlcFastAPIAdapter, config=OlcAdapterConfig(token_number=1))
    return app


class TestFastAPIAdapterASGI(unittest.TestCase):

    def test_allowed_request_passes_through(self):
        app = _make_app(allow=True)
        try:
            client = TestClient(app)
            r = client.get("/ping")
            self.assertEqual(r.status_code, 200)
            self.assertEqual(r.json(), {"ok": True})
        finally:
            for p in app.state._patchers:
                p.stop()

    def test_blocked_request_returns_429(self):
        app = _make_app(allow=False)
        try:
            client = TestClient(app)
            r = client.get("/ping")
            self.assertEqual(r.status_code, 429)
            body = r.json()
            self.assertEqual(body["error"], "Too Many Requests")
            self.assertEqual(body["path"], "/ping")
        finally:
            for p in app.state._patchers:
                p.stop()


class TestBodySizeExtraction(unittest.TestCase):
    """测试请求体大小提取"""

    def test_default_extractor_reads_content_length(self):
        """默认提取器从 Content-Length 头读取"""
        scope = {
            "type": "http",
            "headers": [(b"content-length", b"1024")],
        }
        result = OlcFastAPIAdapter._default_body_size_extractor_from_scope(scope)
        self.assertEqual(result, 1024)

    def test_default_extractor_missing_header_returns_negative_one(self):
        """Content-Length 头缺失时返回 -1"""
        scope = {
            "type": "http",
            "headers": [],
        }
        result = OlcFastAPIAdapter._default_body_size_extractor_from_scope(scope)
        self.assertEqual(result, -1)

    def test_default_extractor_invalid_value_returns_negative_one(self):
        """Content-Length 值非整数时返回 -1"""
        scope = {
            "type": "http",
            "headers": [(b"content-length", b"abc")],
        }
        result = OlcFastAPIAdapter._default_body_size_extractor_from_scope(scope)
        self.assertEqual(result, -1)

    def test_default_extractor_case_insensitive(self):
        """头名大小写不敏感"""
        scope = {
            "type": "http",
            "headers": [(b"Content-Length", b"2048")],
        }
        result = OlcFastAPIAdapter._default_body_size_extractor_from_scope(scope)
        self.assertEqual(result, 2048)

    def test_default_extractor_zero_value(self):
        """Content-Length 为 0 时返回 0"""
        scope = {
            "type": "http",
            "headers": [(b"content-length", b"0")],
        }
        result = OlcFastAPIAdapter._default_body_size_extractor_from_scope(scope)
        self.assertEqual(result, 0)

    def test_default_extractor_no_headers_key(self):
        """scope 无 headers 键时返回 -1"""
        scope = {"type": "http"}
        result = OlcFastAPIAdapter._default_body_size_extractor_from_scope(scope)
        self.assertEqual(result, -1)


class TestBodySizeExtractorCustom(unittest.TestCase):
    """测试自定义 body_size_extractor"""

    def test_custom_sync_extractor(self):
        """自定义同步提取器"""
        from olc.adapters.fastapi import OlcAdapterConfig

        def custom_extractor(request):
            return 4096

        config = OlcAdapterConfig(body_size_extractor=custom_extractor)
        adapter = OlcFastAPIAdapter(app=lambda **kw: None, config=config)

        scope = {"type": "http", "headers": [], "path": "/", "method": "GET"}

        async def receive():
            return {"type": "http.request", "body": b""}

        import asyncio
        result = asyncio.new_event_loop().run_until_complete(
            adapter._extract_request_body_size(scope, receive)
        )
        self.assertEqual(result, 4096)

    def test_custom_async_extractor(self):
        """自定义异步提取器"""
        from olc.adapters.fastapi import OlcAdapterConfig

        async def custom_extractor(request):
            return 8192

        config = OlcAdapterConfig(body_size_extractor=custom_extractor)
        adapter = OlcFastAPIAdapter(app=lambda **kw: None, config=config)

        scope = {"type": "http", "headers": [], "path": "/", "method": "GET"}

        async def receive():
            return {"type": "http.request", "body": b""}

        import asyncio
        result = asyncio.new_event_loop().run_until_complete(
            adapter._extract_request_body_size(scope, receive)
        )
        self.assertEqual(result, 8192)

    def test_custom_extractor_exception_returns_negative_one(self):
        """自定义提取器抛异常时返回 -1"""
        from olc.adapters.fastapi import OlcAdapterConfig

        def custom_extractor(request):
            raise ValueError("test error")

        config = OlcAdapterConfig(body_size_extractor=custom_extractor)
        adapter = OlcFastAPIAdapter(app=lambda **kw: None, config=config)

        scope = {"type": "http", "headers": [], "path": "/", "method": "GET"}

        async def receive():
            return {"type": "http.request", "body": b""}

        import asyncio
        result = asyncio.new_event_loop().run_until_complete(
            adapter._extract_request_body_size(scope, receive)
        )
        self.assertEqual(result, -1)


if __name__ == "__main__":
    unittest.main(verbosity=2)