"""
Tests for the Flask application factory.

Tests cover:
- is_private_ip helper (re-imported from security.network_utils)
- DiskSpoolingRequest class
- create_app function
- CSRF protection
- Error handlers
- Static file serving
"""

import pytest
from unittest.mock import Mock, patch
from flask import Flask


class TestIsPrivateIp:
    """Tests for the is_private_ip helper used by SecureCookieMiddleware."""

    def test_localhost_ipv4(self):
        """127.0.0.1 is private."""
        from local_deep_research.security.network_utils import is_private_ip

        assert is_private_ip("127.0.0.1") is True

    def test_localhost_ipv6(self):
        """::1 is private."""
        from local_deep_research.security.network_utils import is_private_ip

        assert is_private_ip("::1") is True

    def test_private_class_a(self):
        """10.x.x.x is private."""
        from local_deep_research.security.network_utils import is_private_ip

        assert is_private_ip("10.0.0.1") is True
        assert is_private_ip("10.255.255.255") is True

    def test_private_class_b(self):
        """172.16.x.x - 172.31.x.x is private."""
        from local_deep_research.security.network_utils import is_private_ip

        assert is_private_ip("172.16.0.1") is True
        assert is_private_ip("172.31.255.255") is True

    def test_private_class_c(self):
        """192.168.x.x is private."""
        from local_deep_research.security.network_utils import is_private_ip

        assert is_private_ip("192.168.0.1") is True
        assert is_private_ip("192.168.255.255") is True

    def test_public_ip(self):
        """Public IPs are not private."""
        from local_deep_research.security.network_utils import is_private_ip

        assert is_private_ip("8.8.8.8") is False
        assert is_private_ip("1.1.1.1") is False
        assert is_private_ip("142.250.190.78") is False

    def test_invalid_ip(self):
        """Invalid IP returns False."""
        from local_deep_research.security.network_utils import is_private_ip

        assert is_private_ip("invalid") is False
        assert is_private_ip("256.256.256.256") is False
        assert is_private_ip("") is False


class TestDiskSpoolingRequest:
    """Tests for DiskSpoolingRequest class."""

    def test_max_form_memory_size(self):
        """DiskSpoolingRequest has correct memory threshold."""
        from local_deep_research.web.app_factory import DiskSpoolingRequest

        # 5MB threshold
        assert DiskSpoolingRequest.max_form_memory_size == 5 * 1024 * 1024

    def test_inherits_from_request(self):
        """DiskSpoolingRequest inherits from Flask Request."""
        from local_deep_research.web.app_factory import DiskSpoolingRequest
        from flask import Request

        assert issubclass(DiskSpoolingRequest, Request)


class TestCreateApp:
    """Tests for create_app function."""

    def test_returns_flask_app_and_socketio(self):
        """create_app returns Flask app and SocketIO."""
        from local_deep_research.web.app_factory import create_app

        with patch(
            "local_deep_research.web.app_factory.SocketIOService"
        ) as mock_socketio:
            mock_socketio_instance = Mock()
            mock_socketio.return_value.get_socketio.return_value = (
                mock_socketio_instance
            )

            app, socketio = create_app()

            assert isinstance(app, Flask)
            assert socketio is not None

    def test_csrf_protection_enabled(self):
        """CSRF protection is enabled."""
        from local_deep_research.web.app_factory import create_app

        with patch("local_deep_research.web.app_factory.SocketIOService"):
            app, _ = create_app()

            # CSRF extension should be registered
            assert "csrf" in app.extensions

    def test_uses_disk_spooling_request(self):
        """App uses DiskSpoolingRequest class."""
        from local_deep_research.web.app_factory import (
            create_app,
            DiskSpoolingRequest,
        )

        with patch("local_deep_research.web.app_factory.SocketIOService"):
            app, _ = create_app()

            assert app.request_class == DiskSpoolingRequest

    def test_proxy_fix_middleware(self):
        """App has ProxyFix middleware."""
        from local_deep_research.web.app_factory import create_app

        with patch("local_deep_research.web.app_factory.SocketIOService"):
            app, _ = create_app()

            # Check that wsgi_app has been wrapped
            # The actual wsgi_app is wrapped multiple times
            assert app.wsgi_app is not None

    def test_has_static_dir_config(self):
        """App has STATIC_DIR config."""
        from local_deep_research.web.app_factory import create_app

        with patch("local_deep_research.web.app_factory.SocketIOService"):
            app, _ = create_app()

            assert "STATIC_DIR" in app.config

    def test_error_handlers_registered(self):
        """Error handlers are registered."""
        from local_deep_research.web.app_factory import create_app

        with patch("local_deep_research.web.app_factory.SocketIOService"):
            app, _ = create_app()

            # Check that error handlers exist for common codes
            assert app.error_handler_spec is not None


class TestAppRoutes:
    """Tests for routes registered by create_app."""

    @pytest.fixture
    def app(self):
        """Create test app."""
        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
            return app

    @pytest.fixture
    def client(self, app):
        """Create test client."""
        return app.test_client()

    def test_static_route_exists(self, app):
        """Static route is registered."""
        # Check that the static route exists
        rules = [rule.rule for rule in app.url_map.iter_rules()]
        static_routes = [r for r in rules if "static" in r]
        assert len(static_routes) > 0

    def test_index_route_exists(self, app):
        """Index route is registered."""
        rules = [rule.rule for rule in app.url_map.iter_rules()]
        assert "/" in rules

    def test_api_routes_registered(self, app):
        """API routes are registered."""
        rules = [rule.rule for rule in app.url_map.iter_rules()]
        api_routes = [r for r in rules if "/api/" in r]
        assert len(api_routes) > 0


class TestSecurityHeaders:
    """Tests for security headers."""

    @pytest.fixture
    def app(self):
        """Create test app."""
        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
            return app

    @pytest.fixture
    def client(self, app):
        """Create test client."""
        return app.test_client()

    def test_response_has_security_headers(self, client):
        """Responses have security headers."""
        # audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
        response = client.get("/")

        # Check for common security headers
        # Content-Security-Policy or X-Content-Type-Options
        headers = dict(response.headers)
        # At least one security header should be present
        security_headers = [
            "X-Content-Type-Options",
            "X-Frame-Options",
            "Content-Security-Policy",
        ]
        # Note: Security headers may not be set on all routes
        # Just verify the app runs without errors and we can check headers
        _ = any(h in headers for h in security_headers)
        assert response is not None


class TestCsrfProtection:
    """Tests for CSRF protection."""

    @pytest.fixture
    def app(self):
        """Create test app."""
        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
            return app

    def test_csrf_enabled_by_default(self, app):
        """CSRF is enabled by default."""
        assert "csrf" in app.extensions

    def test_csrf_token_endpoint_exists(self, app):
        """CSRF token endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
        rules = [rule.rule for rule in app.url_map.iter_rules()]
        csrf_routes = [r for r in rules if "csrf" in r.lower()]
        # Should have a CSRF token endpoint
        assert len(csrf_routes) >= 0  # May not have explicit route


class TestRateLimiting:
    """Tests for rate limiting."""

    @pytest.fixture
    def app(self):
        """Create test app."""
        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
            return app


class TestErrorHandlers:
    """Tests for error handlers."""

    @pytest.fixture
    def app(self):
        """Create test app with error test routes."""
        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

            # Add test routes that trigger errors
            @app.route("/test-500")
            def trigger_500():
                raise Exception("Test error")

            return app

    @pytest.fixture
    def client(self, app):
        """Create test client."""
        return app.test_client()

    def test_404_returns_json_for_api(self, client):
        """404 returns JSON for API routes."""
        response = client.get("/api/nonexistent-route")

        # Should return 404
        assert response.status_code == 404

    def test_404_returns_html_for_web(self, client):
        """404 returns HTML for web routes."""
        response = client.get("/nonexistent-page")

        # Should return 404
        assert response.status_code == 404


class TestFileUploadSecurity:
    """Tests for file upload security."""

    def test_file_upload_validator_available(self):
        """FileUploadValidator is available."""
        from local_deep_research.security.file_upload_validator import (
            FileUploadValidator,
        )

        validator = FileUploadValidator()
        assert validator is not None

    def test_max_form_memory_prevents_memory_exhaustion(self):
        """DiskSpoolingRequest prevents memory exhaustion.

        Files larger than the threshold are spooled to disk by Werkzeug
        instead of being held in RAM — keeping memory bounded even though
        the per-file cap (FileUploadValidator.MAX_FILE_SIZE) is large.
        """
        from local_deep_research.security.file_upload_validator import (
            FileUploadValidator,
        )
        from local_deep_research.web.app_factory import DiskSpoolingRequest

        # 5MB threshold means files larger than this go to disk.
        threshold = DiskSpoolingRequest.max_form_memory_size
        assert threshold == 5 * 1024 * 1024  # 5MB
        # Sanity: threshold must stay well below the per-file cap, otherwise
        # large uploads would never spool to disk. Reference the configured
        # cap directly so the bound scales when the cap is changed.
        assert threshold < FileUploadValidator.MAX_FILE_SIZE


class TestSecureCookieMiddleware:
    """Tests for SecureCookieMiddleware WSGI middleware.

    The middleware adds the Secure flag iff the request is HTTPS, regardless
    of source IP. ProxyFix translates X-Forwarded-Proto into wsgi.url_scheme
    before this middleware runs.
    """

    def _make_middleware(self, testing_mode=False):
        from local_deep_research.security.web_middleware import (
            SecureCookieMiddleware,
        )

        def inner(environ, start_response):
            start_response(
                "200 OK", [("Set-Cookie", "session=abc; Path=/; HttpOnly")]
            )
            return [b""]

        flask_app = Mock()
        flask_app.config = {"LDR_TESTING_MODE": testing_mode}
        return SecureCookieMiddleware(inner, flask_app)

    def _capture_headers(self, mw, environ):
        captured = {}

        def start_response(status, headers, exc_info=None):
            captured["headers"] = headers

        list(mw(environ, start_response))
        return captured["headers"]

    def test_http_does_not_add_secure_regardless_of_source(self):
        """HTTP requests never get Secure, even from public IPs (#3849)."""
        mw = self._make_middleware()
        for remote_addr in [
            "127.0.0.1",
            "192.168.1.100",
            "172.17.0.2",
            "8.8.8.8",
            # Docker Desktop NAT gateway range that triggered #3849
            "172.67.130.145",
        ]:
            headers = self._capture_headers(
                mw,
                {"REMOTE_ADDR": remote_addr, "wsgi.url_scheme": "http"},
            )
            cookie = next(v for n, v in headers if n.lower() == "set-cookie")
            assert "Secure" not in cookie, (
                f"HTTP from {remote_addr} should not get Secure flag"
            )

    def test_https_adds_secure(self):
        """HTTPS requests always get Secure."""
        mw = self._make_middleware()
        headers = self._capture_headers(
            mw, {"REMOTE_ADDR": "8.8.8.8", "wsgi.url_scheme": "https"}
        )
        cookie = next(v for n, v in headers if n.lower() == "set-cookie")
        assert "Secure" in cookie

    def test_testing_mode_skips_secure_even_on_https(self):
        """LDR_TESTING_MODE disables the Secure flag entirely."""
        mw = self._make_middleware(testing_mode=True)
        headers = self._capture_headers(
            mw, {"REMOTE_ADDR": "8.8.8.8", "wsgi.url_scheme": "https"}
        )
        cookie = next(v for n, v in headers if n.lower() == "set-cookie")
        assert "Secure" not in cookie

    def test_warning_fires_once_per_instance(self):
        """The HTTP-to-public-IP warning is one-shot per middleware instance."""
        mw = self._make_middleware()
        env = {"REMOTE_ADDR": "8.8.8.8", "wsgi.url_scheme": "http"}
        with patch(
            "local_deep_research.security.web_middleware.logger"
        ) as mock_logger:
            self._capture_headers(mw, env)
            self._capture_headers(mw, env)
            self._capture_headers(mw, env)
            warning_calls = [
                c
                for c in mock_logger.warning.call_args_list
                if "Serving HTTP to non-private client" in c.args[0]
            ]
            assert len(warning_calls) == 1

    def test_warning_does_not_fire_for_private_ip(self):
        """No warning when end-user IP is in a private range."""
        mw = self._make_middleware()
        with patch(
            "local_deep_research.security.web_middleware.logger"
        ) as mock_logger:
            self._capture_headers(
                mw,
                {
                    "REMOTE_ADDR": "192.168.1.100",
                    "wsgi.url_scheme": "http",
                },
            )
            warning_calls = [
                c
                for c in mock_logger.warning.call_args_list
                if "Serving HTTP to non-private client" in c.args[0]
            ]
            assert not warning_calls


class TestSessionConfiguration:
    """Tests for session cookie configuration."""

    @pytest.fixture
    def app(self):
        """Create test app."""
        from local_deep_research.web.app_factory import create_app

        with patch("local_deep_research.web.app_factory.SocketIOService"):
            app, _ = create_app()
            return app

    def test_session_cookie_httponly(self, app):
        """Session cookie should have HttpOnly flag."""
        assert app.config["SESSION_COOKIE_HTTPONLY"] is True

    def test_session_cookie_samesite(self, app):
        """Session cookie should have SameSite=Lax."""
        assert app.config["SESSION_COOKIE_SAMESITE"] == "Lax"

    def test_permanent_session_lifetime(self, app):
        """Session should have 30-day lifetime."""
        assert app.config["PERMANENT_SESSION_LIFETIME"] == 30 * 24 * 3600

    def test_preferred_url_scheme_https(self, app):
        """Preferred URL scheme should be https."""
        assert app.config["PREFERRED_URL_SCHEME"] == "https"

    def test_wtf_csrf_enabled(self, app):
        """WTF CSRF should be enabled."""
        assert app.config["WTF_CSRF_ENABLED"] is True


class TestBlueprintRegistration:
    """Tests for blueprint registration."""

    @pytest.fixture
    def app(self):
        """Create test app."""
        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
            return app

    def test_auth_blueprint_registered(self, app):
        """Auth blueprint should be registered."""
        rules = [rule.rule for rule in app.url_map.iter_rules()]
        auth_routes = [r for r in rules if "/auth/" in r]
        assert len(auth_routes) > 0

    def test_research_blueprint_registered(self, app):
        """Research blueprint should be registered."""
        rules = [rule.rule for rule in app.url_map.iter_rules()]
        research_routes = [r for r in rules if "/research" in r]
        assert len(research_routes) > 0

    def test_settings_blueprint_registered(self, app):
        """Settings blueprint should be registered."""
        # audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
        rules = [rule.rule for rule in app.url_map.iter_rules()]
        settings_routes = [r for r in rules if "/settings" in r]
        assert len(settings_routes) >= 0  # May or may not exist

    def test_library_blueprint_registered(self, app):
        """Library blueprint should be registered."""
        # audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
        rules = [rule.rule for rule in app.url_map.iter_rules()]
        library_routes = [r for r in rules if "/library" in r]
        assert len(library_routes) >= 0  # May or may not exist
