# Copyright 2025 Softwell S.r.l.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for request lifecycle — add_cleanup, add_onerr, lazy db property."""

from unittest.mock import MagicMock

import pytest
from genro_toolbox import DictObj

from genro_asgi.request import HttpRequest


class TestRequestCleanup:
    """Tests for add_cleanup and run_cleanups."""

    def test_add_cleanup_runs_on_end(self) -> None:
        """Cleanup callbacks run when run_cleanups is called."""
        request = HttpRequest()
        called = []
        request.add_cleanup(lambda: called.append("a"))
        request.add_cleanup(lambda: called.append("b"))

        request.run_cleanups()

        assert called == ["b", "a"]  # reverse order

    def test_cleanup_runs_even_on_error(self) -> None:
        """Cleanup callbacks run regardless of error."""
        request = HttpRequest()
        called = []
        request.add_cleanup(lambda: called.append("cleanup"))

        request.run_cleanups(error=ValueError("boom"))

        assert called == ["cleanup"]

    def test_onerr_runs_only_on_error(self) -> None:
        """Onerr callbacks run only when error is not None."""
        request = HttpRequest()
        errors = []
        request.add_onerr(lambda e: errors.append(str(e)))

        request.run_cleanups(error=None)
        assert errors == []

        request.run_cleanups(error=ValueError("boom"))
        assert errors == ["boom"]

    def test_onerr_and_cleanup_both_run(self) -> None:
        """Both onerr and cleanup run on error."""
        request = HttpRequest()
        called = []
        request.add_onerr(lambda e: called.append("onerr"))
        request.add_cleanup(lambda: called.append("cleanup"))

        request.run_cleanups(error=ValueError("boom"))

        assert "onerr" in called
        assert "cleanup" in called

    def test_cleanup_exception_does_not_block_others(self) -> None:
        """One failing cleanup does not prevent others from running."""
        request = HttpRequest()
        called = []

        def bad_cleanup():
            raise RuntimeError("fail")

        request.add_cleanup(lambda: called.append("after"))
        request.add_cleanup(bad_cleanup)
        request.add_cleanup(lambda: called.append("before"))

        request.run_cleanups()

        assert "before" in called
        assert "after" in called


class TestLazyDbProperty:
    """Tests for HttpRequest.db lazy property."""

    @pytest.mark.asyncio
    async def test_db_property_returns_ctx_db(self) -> None:
        """request.db returns db from scope ctx._db."""
        mock_db = MagicMock()
        ctx = DictObj()
        ctx._db = mock_db

        request = HttpRequest()
        await request.init(
            scope={"type": "http", "headers": [], "ctx": ctx},
            receive=MagicMock(),
        )

        assert request.db is mock_db

    @pytest.mark.asyncio
    async def test_db_property_cached(self) -> None:
        """Second access returns cached db, not re-fetched."""
        mock_db = MagicMock()
        ctx = DictObj()
        ctx._db = mock_db

        request = HttpRequest()
        await request.init(
            scope={"type": "http", "headers": [], "ctx": ctx},
            receive=MagicMock(),
        )

        db1 = request.db
        db2 = request.db
        assert db1 is db2

    @pytest.mark.asyncio
    async def test_db_registers_cleanup(self) -> None:
        """Accessing db registers closeConnection as cleanup."""
        mock_db = MagicMock()
        ctx = DictObj()
        ctx._db = mock_db

        request = HttpRequest()
        await request.init(
            scope={"type": "http", "headers": [], "ctx": ctx},
            receive=MagicMock(),
        )

        _ = request.db
        request.run_cleanups()

        mock_db.closeConnection.assert_called_once()

    @pytest.mark.asyncio
    async def test_no_db_access_no_cleanup(self) -> None:
        """If db is never accessed, no cleanup is registered."""
        mock_server = MagicMock()
        mock_db = MagicMock()
        mock_server.get_db.return_value = mock_db
        mock_server.apps = {}

        request = HttpRequest()
        await request.init(
            scope={"type": "http", "headers": []},
            receive=MagicMock(),
            server=mock_server,
        )

        request.run_cleanups()

        mock_db.closeConnection.assert_not_called()

    @pytest.mark.asyncio
    async def test_server_property(self) -> None:
        """request.server returns the server passed at init."""
        mock_server = MagicMock()

        request = HttpRequest()
        await request.init(
            scope={"type": "http", "headers": []},
            receive=MagicMock(),
            server=mock_server,
        )

        assert request.server is mock_server
