"""
Tests for error handler *behavior* in app_factory.register_error_handlers().

Existing tests cover registration and 404 content; these test:
- 401 handler JSON vs redirect branching
- 413 handler JSON vs text branching
- 500 handler JSON vs text branching
- WebAPIException handler JSON conversion
- CSRF handler IP-based messaging (private/public/proxied/HTTPS)
- NewsAPIException handler JSON conversion
- _is_private_ip edge cases (IPv4-mapped IPv6, link-local, multicast)
"""

import pytest
from unittest.mock import patch

from flask import abort


@pytest.fixture
def app():
    """Create a test app with error handlers registered."""
    from local_deep_research.web.app_factory import create_app

    with patch("local_deep_research.web.app_factory.SocketIOService"):
        app, _ = create_app()
        app.config["TESTING"] = True
        app.config["WTF_CSRF_ENABLED"] = False
        # Must disable exception propagation so error handlers are invoked
        app.config["PROPAGATE_EXCEPTIONS"] = False
        app.config["TRAP_HTTP_EXCEPTIONS"] = False

        # Routes that trigger 401 errors
        @app.route("/api/trigger-401")
        def api_trigger_401():
            abort(401)

        @app.route("/settings/api/trigger-401")
        def settings_api_trigger_401():
            abort(401)

        @app.route("/trigger-401")
        def web_trigger_401():
            abort(401)

        # Routes that trigger 413 errors
        @app.route("/api/trigger-413")
        def api_trigger_413():
            abort(413)

        @app.route("/trigger-413")
        def web_trigger_413():
            abort(413)

        # Routes that trigger 500 errors
        @app.route("/api/trigger-500")
        def api_trigger_500():
            raise Exception("boom")

        @app.route("/trigger-500")
        def web_trigger_500():
            raise Exception("boom")

        # Route that raises WebAPIException
        @app.route("/api/trigger-web-api-error")
        def trigger_web_api_error():
            from local_deep_research.web.exceptions import (
                WebAPIException,
            )

            raise WebAPIException(
                message="Something went wrong",
                status_code=400,
                error_code="BAD_REQUEST",
                details={"field": "name"},
            )

        # Route that raises AuthenticationRequiredError
        @app.route("/api/trigger-auth-error")
        def trigger_auth_error():
            from local_deep_research.web.exceptions import (
                AuthenticationRequiredError,
            )

            raise AuthenticationRequiredError()

        # Route that raises NewsAPIException
        @app.route("/api/trigger-news-error")
        def trigger_news_error():
            from local_deep_research.news.exceptions import (
                NewsAPIException,
            )

            raise NewsAPIException(
                message="Feed unavailable",
                status_code=503,
                error_code="FEED_ERROR",
            )

        return app


@pytest.fixture
def client(app):
    return app.test_client()


class TestUnauthorizedHandler:
    """Tests for the 401 error handler behavior."""

    def test_401_returns_json_for_api_path(self, client):
        """API paths get JSON error response for 401."""
        response = client.get("/api/trigger-401")
        assert response.status_code == 401
        data = response.get_json()
        assert data is not None
        assert data["error"] == "Authentication required"

    def test_401_returns_json_for_settings_api_path(self, client):
        """/settings/api/ paths get JSON error response for 401."""
        response = client.get("/settings/api/trigger-401")
        assert response.status_code == 401
        data = response.get_json()
        assert data is not None
        assert data["error"] == "Authentication required"

    def test_401_redirects_for_web_path(self, client):
        """Non-API paths redirect to login page for 401."""
        response = client.get("/trigger-401")
        assert response.status_code == 302
        assert "/auth/login" in response.location


class TestRequestTooLargeHandler:
    """Tests for the 413 error handler behavior."""

    def test_413_returns_json_for_api_path(self, client):
        """API paths get JSON error response for 413."""
        response = client.get("/api/trigger-413")
        assert response.status_code == 413
        data = response.get_json()
        assert data is not None
        assert data["error"] == "Request too large"

    def test_413_returns_text_for_web_path(self, client):
        """Non-API paths get plain text error response for 413."""
        response = client.get("/trigger-413")
        assert response.status_code == 413
        assert response.data == b"Request too large"


class TestServerErrorHandler:
    """Tests for the 500 error handler behavior."""

    def test_500_returns_json_for_api_path(self, client):
        """API paths get JSON error response for 500."""
        response = client.get("/api/trigger-500")
        assert response.status_code == 500
        data = response.get_json()
        assert data is not None
        assert data["error"] == "Server error"

    def test_500_returns_text_for_web_path(self, client):
        """Non-API paths get plain text error response for 500."""
        response = client.get("/trigger-500")
        assert response.status_code == 500
        assert response.data == b"Server error"


class TestCsrfErrorHandler:
    """Tests for CSRF error handler IP-based branching."""

    def _trigger_csrf(
        self, app, remote_addr, is_secure=False, forwarded_for=None
    ):
        """Trigger a CSRF error with controlled request environment."""
        from flask_wtf.csrf import CSRFError

        # Must set PREFERRED_URL_SCHEME to match is_secure, because Flask
        # uses it to determine request.scheme / request.is_secure
        app.config["PREFERRED_URL_SCHEME"] = "https" if is_secure else "http"

        ctx_kwargs = {
            "environ_base": {
                "REMOTE_ADDR": remote_addr,
                "wsgi.url_scheme": "https" if is_secure else "http",
            },
        }
        if forwarded_for:
            ctx_kwargs["headers"] = {"X-Forwarded-For": forwarded_for}

        with app.test_request_context("/api/test", **ctx_kwargs):
            # CSRF handler is registered under status code 400
            handlers_400 = app.error_handler_spec.get(None, {}).get(400, {})
            handler = handlers_400.get(CSRFError)
            if handler is None:
                pytest.skip("CSRFError handler not registered")
            error = CSRFError("CSRF token missing")
            response = handler(error)
            return response

    def test_csrf_error_returns_description_as_json(self, app):
        """CSRF errors return the error description as JSON, regardless of
        source IP or scheme. The old IP-based 'use HTTPS' branch is gone."""
        response = self._trigger_csrf(app, "8.8.8.8")
        assert response.status_code == 400
        data = response.get_json()
        assert "error" in data
        # No IP-specific guidance is added anymore
        assert "Solutions" not in data["error"]
        assert "8.8.8.8" not in data["error"]

    def test_csrf_returns_400_status(self, app):
        """CSRF errors return 400 status code."""
        from flask_wtf.csrf import CSRFError

        with app.test_request_context("/api/test"):
            handlers_400 = app.error_handler_spec.get(None, {}).get(400, {})
            handler = handlers_400.get(CSRFError)
            if handler is None:
                pytest.skip("CSRFError handler not registered")
            response = handler(CSRFError("bad token"))
            assert response.status_code == 400


class TestNewsAPIExceptionHandler:
    """Tests for NewsAPIException error handler."""

    def test_news_exception_returns_json(self, client):
        """NewsAPIException is converted to JSON with error_code and status."""
        response = client.get("/api/trigger-news-error")
        assert response.status_code == 503
        data = response.get_json()
        assert data["error"] == "Feed unavailable"
        assert data["error_code"] == "FEED_ERROR"
        assert data["status_code"] == 503


class TestWebAPIExceptionHandler:
    """Tests for WebAPIException error handler."""

    def test_web_api_exception_returns_json(self, client):
        """WebAPIException is converted to JSON with to_dict() fields."""
        response = client.get("/api/trigger-web-api-error")
        assert response.status_code == 400
        data = response.get_json()
        assert data is not None
        assert data["status"] == "error"
        assert data["message"] == "Something went wrong"
        assert data["error_code"] == "BAD_REQUEST"
        assert data["details"] == {"field": "name"}

    def test_web_api_exception_preserves_status_code(self, client):
        """Response uses the exception's status_code, not a default."""
        response = client.get("/api/trigger-web-api-error")
        assert response.status_code == 400

    def test_authentication_required_error_returns_401(self, client):
        """AuthenticationRequiredError returns 401 with structured JSON."""
        response = client.get("/api/trigger-auth-error")
        assert response.status_code == 401
        data = response.get_json()
        assert data is not None
        assert data["status"] == "error"
        assert "Authentication required" in data["message"]
        assert data["error_code"] == "AUTHENTICATION_REQUIRED"


class TestIsPrivateIpEdgeCases:
    """Edge cases for _is_private_ip not covered by existing tests."""

    def test_ipv4_mapped_ipv6_private(self):
        """IPv4-mapped IPv6 address with private IPv4 is treated as private."""
        from local_deep_research.web.app_factory import _is_private_ip

        assert _is_private_ip("::ffff:192.168.1.1") is True

    def test_link_local_address(self):
        """Link-local addresses (169.254.x.x) are private."""
        from local_deep_research.web.app_factory import _is_private_ip

        # 169.254.x.x is link-local, treated as private by ipaddress module
        assert _is_private_ip("169.254.1.1") is True

    def test_multicast_address(self):
        """Multicast addresses (224.x.x.x) are not private/loopback."""
        from local_deep_research.web.app_factory import _is_private_ip
        import ipaddress

        ip = ipaddress.ip_address("224.0.0.1")
        expected = ip.is_private or ip.is_loopback
        assert _is_private_ip("224.0.0.1") is expected
