# allow: no-sut-import — black-box HTTP test; drives real routes through the Flask test client
"""
Security tests for chat feature.

Tests cross-user access prevention and authentication requirements at API layer.
This complements the existing user isolation tests (test_chat_user_isolation.py)
which test at the service layer.
"""

import json


class TestSessionOwnershipSecurity:
    """
    Tests verifying session ownership is enforced at the API layer.

    Note: In LDR's architecture, each user has a separate encrypted database,
    so cross-user access is prevented at the database level. These tests verify
    that the API layer correctly uses the authenticated user's database.
    """

    def test_user_cannot_access_other_user_session_via_api(
        self, authenticated_client, second_user_client
    ):
        """
        Test that one user cannot access another user's session via API.

        Due to LDR's per-user database architecture, "other user's session"
        simply won't exist in the current user's database.
        """
        # Create a session as first user
        create_response = authenticated_client.post(
            "/api/chat/sessions",
            json={"initial_query": "User 1's research question"},
            content_type="application/json",
        )
        assert create_response.status_code == 200
        data = json.loads(create_response.data)
        session_id = data["session_id"]

        # Try to access the session as second user - should get 404 (not found in their DB)
        response = second_user_client.get(f"/api/chat/sessions/{session_id}")
        assert response.status_code == 404
        data = json.loads(response.data)
        assert data["success"] is False
        assert "not found" in data["error"].lower()

    def test_user_cannot_list_other_user_sessions(
        self, authenticated_client, second_user_client
    ):
        """Test that listing sessions only returns the current user's sessions."""
        # Create sessions as first user
        for i in range(3):
            authenticated_client.post(
                "/api/chat/sessions",
                json={"initial_query": f"User 1's query {i}"},
                content_type="application/json",
            )

        # List sessions as second user - should be empty or not contain first user's sessions
        response = second_user_client.get("/api/chat/sessions")
        assert response.status_code == 200
        data = json.loads(response.data)
        assert data["success"] is True
        # Second user should have no sessions (or only their own)
        # The first user's 3 sessions should not appear
        sessions = data["sessions"]
        # Since second_user just registered, they shouldn't have any sessions
        assert len(sessions) == 0

    def test_user_cannot_send_message_to_other_user_session(
        self, authenticated_client, second_user_client
    ):
        """Test that one user cannot send messages to another user's session."""
        # Create session as first user
        create_response = authenticated_client.post(
            "/api/chat/sessions",
            json={"initial_query": "First user's topic"},
            content_type="application/json",
        )
        session_id = json.loads(create_response.data)["session_id"]

        # Try to send message to that session as second user
        response = second_user_client.post(
            f"/api/chat/sessions/{session_id}/messages",
            json={"content": "Malicious message", "trigger_research": False},
            content_type="application/json",
        )
        # Should get 404 (session not found in their DB)
        assert response.status_code == 404
        data = json.loads(response.data)
        assert data["success"] is False

    def test_session_enumeration_returns_404_not_403(
        self, authenticated_client, second_user_client
    ):
        """
        Test that attempting to access another user's session returns 404, not 403.

        This prevents session enumeration attacks - a 403 would reveal that
        the session exists but belongs to another user, while 404 reveals nothing.
        """
        # Create session as first user
        create_response = authenticated_client.post(
            "/api/chat/sessions",
            json={"initial_query": "Secret research"},
            content_type="application/json",
        )
        session_id = json.loads(create_response.data)["session_id"]

        # Access as second user - should get 404, NOT 403
        response = second_user_client.get(f"/api/chat/sessions/{session_id}")
        # The critical assertion: 404 not 403
        assert response.status_code == 404
        # Double-check: response should not reveal the session exists
        data = json.loads(response.data)
        assert "forbidden" not in data.get("error", "").lower()
        assert "permission" not in data.get("error", "").lower()

    def test_user_cannot_update_other_user_session(
        self, authenticated_client, second_user_client
    ):
        """Test that one user cannot update another user's session."""
        # Create session as first user
        create_response = authenticated_client.post(
            "/api/chat/sessions",
            json={"initial_query": "Original topic"},
            content_type="application/json",
        )
        session_id = json.loads(create_response.data)["session_id"]

        # Try to update as second user
        hijack_response = second_user_client.patch(
            f"/api/chat/sessions/{session_id}",
            json={"title": "Hijacked title"},
            content_type="application/json",
        )
        # Session does not exist in second user's per-user DB, so the route
        # should report 404 — assert the status code so a regression that
        # quietly changes this contract is caught here, not just by the
        # downstream side-effect check.
        assert hijack_response.status_code == 404

        # Verify original user's session is unchanged
        verify_response = authenticated_client.get(
            f"/api/chat/sessions/{session_id}"
        )
        verify_data = json.loads(verify_response.data)
        # Title should still be based on original query, not "Hijacked title"
        assert verify_data["session"]["title"] != "Hijacked title"

    def test_user_cannot_delete_other_user_session(
        self, authenticated_client, second_user_client
    ):
        """Test that one user cannot delete another user's session."""
        # Create session as first user
        create_response = authenticated_client.post(
            "/api/chat/sessions",
            json={"initial_query": "Important research"},
            content_type="application/json",
        )
        session_id = json.loads(create_response.data)["session_id"]

        # Try to delete as second user
        second_user_client.delete(f"/api/chat/sessions/{session_id}")
        # Operation either fails or has no effect (session not in their DB)

        # Verify original user's session still exists
        verify_response = authenticated_client.get(
            f"/api/chat/sessions/{session_id}"
        )
        assert verify_response.status_code == 200
        verify_data = json.loads(verify_response.data)
        assert verify_data["success"] is True
        # Session should still be active
        assert verify_data["session"]["status"] == "active"


class TestAuthenticationRequired:
    """Tests verifying authentication is required for chat endpoints."""

    def test_chat_page_redirects_without_login(self, app):
        """Test that chat page redirects to login when not authenticated."""
        with app.test_client() as client:
            response = client.get("/chat/")
            # Should redirect to login
            assert response.status_code == 302
            assert "/auth/login" in response.location

    def test_chat_page_with_session_id_redirects_without_login(self, app):
        """Test that chat page with session ID redirects to login when not authenticated."""
        with app.test_client() as client:
            response = client.get("/chat/some-session-id")
            assert response.status_code == 302
            assert "/auth/login" in response.location

    def test_create_session_api_requires_authentication(self, app):
        """Test that creating a session requires authentication."""
        with app.test_client() as client:
            response = client.post(
                "/api/chat/sessions",
                json={"initial_query": "Test query"},
                content_type="application/json",
            )
            assert response.status_code == 401
            data = json.loads(response.data)
            assert "error" in data
            assert "authentication" in data["error"].lower()

    def test_list_sessions_api_requires_authentication(self, app):
        """Test that listing sessions requires authentication."""
        with app.test_client() as client:
            response = client.get("/api/chat/sessions")
            assert response.status_code == 401
            data = json.loads(response.data)
            assert "error" in data
            assert "authentication" in data["error"].lower()

    def test_get_session_api_requires_authentication(self, app):
        """Test that getting a session requires authentication."""
        with app.test_client() as client:
            response = client.get("/api/chat/sessions/some-id")
            assert response.status_code == 401
            data = json.loads(response.data)
            assert "error" in data
            assert "authentication" in data["error"].lower()

    def test_update_session_api_requires_authentication(self, app):
        """Test that updating a session requires authentication."""
        with app.test_client() as client:
            response = client.patch(
                "/api/chat/sessions/some-id",
                json={"title": "New title"},
                content_type="application/json",
            )
            assert response.status_code == 401
            data = json.loads(response.data)
            assert "error" in data
            assert "authentication" in data["error"].lower()

    def test_delete_session_api_requires_authentication(self, app):
        """Test that deleting a session requires authentication."""
        with app.test_client() as client:
            response = client.delete("/api/chat/sessions/some-id")
            assert response.status_code == 401
            data = json.loads(response.data)
            assert "error" in data
            assert "authentication" in data["error"].lower()

    def test_get_messages_api_requires_authentication(self, app):
        """Test that getting messages requires authentication."""
        with app.test_client() as client:
            response = client.get("/api/chat/sessions/some-id/messages")
            assert response.status_code == 401
            data = json.loads(response.data)
            assert "error" in data
            assert "authentication" in data["error"].lower()

    def test_send_message_api_requires_authentication(self, app):
        """Test that sending messages requires authentication."""
        with app.test_client() as client:
            response = client.post(
                "/api/chat/sessions/some-id/messages",
                json={"content": "Test message"},
                content_type="application/json",
            )
            assert response.status_code == 401
            data = json.loads(response.data)
            assert "error" in data
            assert "authentication" in data["error"].lower()


class TestSessionIdValidation:
    """Tests for session ID validation to prevent injection attacks."""

    def test_session_id_with_path_traversal_rejected(
        self, authenticated_client
    ):
        """Test that session IDs with path traversal are handled safely."""
        malicious_ids = [
            "../../../etc/passwd",
            "..%2F..%2Fetc%2Fpasswd",
            "session-id/../../admin",
            "valid-id/../other-id",
        ]

        for session_id in malicious_ids:
            response = authenticated_client.get(
                f"/api/chat/sessions/{session_id}"
            )
            # Path-traversal attempts should be normalized by Flask routing
            # and surface as 404 (no such session) or 400 (invalid id).
            # 500 would be an unhandled-exception regression. Importantly,
            # the response body must never echo system file paths.
            assert response.status_code in [404, 400]
            data = json.loads(response.data)
            assert "etc" not in str(data).lower()
            assert "passwd" not in str(data).lower()

    def test_session_id_with_null_bytes_rejected(self, authenticated_client):
        """Test that session IDs with null bytes are handled safely."""
        # URL-encoded null byte
        response = authenticated_client.get(
            "/api/chat/sessions/valid-id%00.txt"
        )
        # Null-byte-tainted IDs should be rejected with 404 or 400.
        # 500 would be an unhandled-exception regression.
        assert response.status_code in [404, 400]

    def test_session_id_with_sql_injection_handled(self, authenticated_client):
        """Test that SQL injection attempts in session ID are handled safely."""
        injection_attempts = [
            "' OR '1'='1",
            "'; DROP TABLE chat_sessions; --",
            "1 UNION SELECT * FROM users--",
        ]

        for injection in injection_attempts:
            response = authenticated_client.get(
                f"/api/chat/sessions/{injection}"
            )
            # Should return 404 (not found) - the injection string is just a literal ID
            assert response.status_code == 404
            data = json.loads(response.data)
            # Should not contain SQL error messages
            assert "syntax" not in str(data).lower()
            assert "sqlite" not in str(data).lower()
            assert "database" not in str(data).lower()
