# API Security Test Suite
# mockupAWS v1.0.0
#
# This test suite covers API-specific security testing including:
# - Authentication bypass attempts
# - Authorization checks
# - Injection attacks (SQL, NoSQL, Command)
# - Rate limiting validation
# - Input validation
# - CSRF protection
# - CORS configuration

import pytest
import requests
import json
import time
from typing import Dict, Any
import jwt

# Configuration
BASE_URL = "http://localhost:8000"
API_V1 = f"{BASE_URL}/api/v1"
INGEST_URL = f"{BASE_URL}/ingest"


class TestAPISecurity:
    """API Security Tests for mockupAWS v1.0.0"""

    @pytest.fixture
    def auth_token(self):
        """Get a valid authentication token"""
        # This would typically create a test user and login
        # For now, returning a mock token structure
        return "mock_token"

    @pytest.fixture
    def api_headers(self, auth_token):
        """Get API headers with authentication"""
        return {
            "Authorization": f"Bearer {auth_token}",
            "Content-Type": "application/json",
        }

    # ============================================
    # AUTHENTICATION TESTS
    # ============================================

    def test_login_with_invalid_credentials(self):
        """Test that invalid credentials are rejected"""
        response = requests.post(
            f"{API_V1}/auth/login",
            json={"username": "invalid@example.com", "password": "wrongpassword"},
        )
        assert response.status_code == 401
        assert "error" in response.json() or "detail" in response.json()

    def test_login_sql_injection_attempt(self):
        """Test SQL injection in login form"""
        malicious_inputs = [
            "admin' OR '1'='1",
            "admin'--",
            "admin'/*",
            "' OR 1=1--",
            "'; DROP TABLE users; --",
        ]

        for payload in malicious_inputs:
            response = requests.post(
                f"{API_V1}/auth/login", json={"username": payload, "password": payload}
            )
            # Should either return 401 or 422 (validation error)
            assert response.status_code in [401, 422]

    def test_access_protected_endpoint_without_auth(self):
        """Test that protected endpoints require authentication"""
        protected_endpoints = [
            f"{API_V1}/scenarios",
            f"{API_V1}/metrics/dashboard",
            f"{API_V1}/reports",
        ]

        for endpoint in protected_endpoints:
            response = requests.get(endpoint)
            assert response.status_code in [401, 403], (
                f"Endpoint {endpoint} should require auth"
            )

    def test_malformed_jwt_token(self):
        """Test handling of malformed JWT tokens"""
        malformed_tokens = [
            "not.a.token",
            "Bearer ",
            "Bearer invalid_token",
            "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.invalid",
        ]

        for token in malformed_tokens:
            headers = {"Authorization": f"Bearer {token}"}
            response = requests.get(f"{API_V1}/scenarios", headers=headers)
            assert response.status_code in [401, 403, 422]

    def test_expired_jwt_token(self):
        """Test handling of expired JWT tokens"""
        # Create an expired token
        expired_token = jwt.encode(
            {"sub": "test", "exp": 0}, "secret", algorithm="HS256"
        )

        headers = {"Authorization": f"Bearer {expired_token}"}
        response = requests.get(f"{API_V1}/scenarios", headers=headers)
        assert response.status_code in [401, 403]

    # ============================================
    # AUTHORIZATION TESTS
    # ============================================

    def test_access_other_user_scenario(self, api_headers):
        """Test that users cannot access other users' scenarios"""
        # Try to access a scenario ID that doesn't belong to user
        response = requests.get(
            f"{API_V1}/scenarios/00000000-0000-0000-0000-000000000000",
            headers=api_headers,
        )
        assert response.status_code in [403, 404]

    def test_modify_other_user_scenario(self, api_headers):
        """Test that users cannot modify other users' scenarios"""
        response = requests.put(
            f"{API_V1}/scenarios/00000000-0000-0000-0000-000000000000",
            headers=api_headers,
            json={"name": "Hacked"},
        )
        assert response.status_code in [403, 404]

    def test_delete_other_user_scenario(self, api_headers):
        """Test that users cannot delete other users' scenarios"""
        response = requests.delete(
            f"{API_V1}/scenarios/00000000-0000-0000-0000-000000000000",
            headers=api_headers,
        )
        assert response.status_code in [403, 404]

    # ============================================
    # INPUT VALIDATION TESTS
    # ============================================

    def test_xss_in_scenario_name(self, api_headers):
        """Test XSS protection in scenario names"""
        xss_payloads = [
            "<script>alert('xss')</script>",
            "<img src=x onerror=alert('xss')>",
            "javascript:alert('xss')",
            "<iframe src='javascript:alert(1)'>",
        ]

        for payload in xss_payloads:
            response = requests.post(
                f"{API_V1}/scenarios",
                headers=api_headers,
                json={"name": payload, "region": "us-east-1", "tags": []},
            )
            # Should either sanitize or reject
            if response.status_code == 201:
                data = response.json()
                # Check that payload is sanitized
                assert "<script>" not in data.get("name", "")
                assert "javascript:" not in data.get("name", "")

    def test_sql_injection_in_search(self, api_headers):
        """Test SQL injection in search parameters"""
        sql_payloads = [
            "' OR '1'='1",
            "'; DROP TABLE scenarios; --",
            "' UNION SELECT * FROM users --",
            "1' AND 1=1--",
        ]

        for payload in sql_payloads:
            response = requests.get(
                f"{API_V1}/scenarios?search={payload}", headers=api_headers
            )
            # Should not return all data or error
            assert response.status_code in [200, 422]
            if response.status_code == 200:
                # Response should be normal, not containing other users' data
                data = response.json()
                assert isinstance(data, dict) or isinstance(data, list)

    def test_nosql_injection_attempt(self, api_headers):
        """Test NoSQL injection attempts"""
        nosql_payloads = [
            {"name": {"$ne": None}},
            {"name": {"$gt": ""}},
            {"$where": "this.name == 'test'"},
        ]

        for payload in nosql_payloads:
            response = requests.post(
                f"{API_V1}/scenarios", headers=api_headers, json=payload
            )
            # Should be rejected or sanitized
            assert response.status_code in [201, 400, 422]

    def test_oversized_payload(self, api_headers):
        """Test handling of oversized payloads"""
        oversized_payload = {
            "name": "A" * 10000,  # Very long name
            "description": "B" * 100000,  # Very long description
            "region": "us-east-1",
            "tags": ["tag"] * 1000,  # Too many tags
        }

        response = requests.post(
            f"{API_V1}/scenarios", headers=api_headers, json=oversized_payload
        )
        # Should reject or truncate
        assert response.status_code in [201, 400, 413, 422]

    def test_invalid_content_type(self):
        """Test handling of invalid content types"""
        headers = {"Content-Type": "text/plain"}
        response = requests.post(
            f"{API_V1}/auth/login", headers=headers, data="not json"
        )
        assert response.status_code in [400, 415, 422]

    # ============================================
    # RATE LIMITING TESTS
    # ============================================

    def test_login_rate_limiting(self):
        """Test rate limiting on login endpoint"""
        # Make many rapid login attempts
        responses = []
        for i in range(10):
            response = requests.post(
                f"{API_V1}/auth/login",
                json={"username": f"user{i}@example.com", "password": "wrong"},
            )
            responses.append(response.status_code)

        # At some point, should get rate limited
        assert 429 in responses or responses.count(401) == len(responses)

    def test_api_key_rate_limiting(self, api_headers):
        """Test rate limiting on API endpoints"""
        responses = []
        for i in range(150):  # Assuming 100 req/min limit
            response = requests.get(f"{API_V1}/scenarios", headers=api_headers)
            responses.append(response.status_code)
            if response.status_code == 429:
                break

        # Should eventually get rate limited
        assert 429 in responses

    def test_ingest_rate_limiting(self):
        """Test rate limiting on ingest endpoint"""
        responses = []
        for i in range(1100):  # Assuming 1000 req/min limit
            response = requests.post(
                INGEST_URL,
                json={"message": f"Test {i}", "source": "rate-test"},
                headers={"X-Scenario-ID": "test-scenario"},
            )
            responses.append(response.status_code)
            if response.status_code == 429:
                break

        # Should get rate limited
        assert 429 in responses

    # ============================================
    # INJECTION TESTS
    # ============================================

    def test_command_injection_in_logs(self):
        """Test command injection in log messages"""
        cmd_injection_payloads = [
            "$(whoami)",
            "`whoami`",
            "; cat /etc/passwd",
            "| ls -la",
            "&& echo pwned",
        ]

        for payload in cmd_injection_payloads:
            response = requests.post(
                INGEST_URL,
                json={"message": payload, "source": "injection-test"},
                headers={"X-Scenario-ID": "test-scenario"},
            )
            # Should accept but sanitize
            assert response.status_code in [200, 202]

    def test_path_traversal_attempts(self, api_headers):
        """Test path traversal in file operations"""
        traversal_payloads = [
            "../../../etc/passwd",
            "..\\..\\..\\windows\\system32\\config\\sam",
            "/etc/passwd",
            "....//....//....//etc/passwd",
        ]

        for payload in traversal_payloads:
            response = requests.get(
                f"{API_V1}/reports/download?file={payload}", headers=api_headers
            )
            # Should not allow file access
            assert response.status_code in [400, 403, 404]

    def test_ssrf_attempts(self, api_headers):
        """Test Server-Side Request Forgery attempts"""
        ssrf_payloads = [
            "http://localhost:8000/admin",
            "http://127.0.0.1:8000/internal",
            "http://169.254.169.254/latest/meta-data/",
            "file:///etc/passwd",
        ]

        for payload in ssrf_payloads:
            response = requests.post(
                f"{API_V1}/scenarios",
                headers=api_headers,
                json={
                    "name": "SSRF Test",
                    "description": payload,
                    "region": "us-east-1",
                    "tags": [],
                },
            )
            # Should not trigger external requests
            if response.status_code == 201:
                data = response.json()
                # Description should not be a URL
                assert not data.get("description", "").startswith(
                    ("http://", "https://", "file://")
                )

    # ============================================
    # CORS TESTS
    # ============================================

    def test_cors_preflight(self):
        """Test CORS preflight requests"""
        response = requests.options(
            f"{API_V1}/scenarios",
            headers={
                "Origin": "http://malicious-site.com",
                "Access-Control-Request-Method": "POST",
                "Access-Control-Request-Headers": "Content-Type",
            },
        )

        # Should not allow arbitrary origins
        assert response.status_code in [200, 204]
        allowed_origin = response.headers.get("Access-Control-Allow-Origin", "")
        assert "malicious-site.com" not in allowed_origin

    def test_cors_headers(self, api_headers):
        """Test CORS headers on actual requests"""
        response = requests.get(
            f"{API_V1}/scenarios", headers={**api_headers, "Origin": "http://evil.com"}
        )

        allowed_origin = response.headers.get("Access-Control-Allow-Origin", "")
        # Should not reflect arbitrary origins
        assert "evil.com" not in allowed_origin

    # ============================================
    # API KEY SECURITY TESTS
    # ============================================

    def test_api_key_exposure_in_response(self, api_headers):
        """Test that API keys are not exposed in responses"""
        # Create an API key
        response = requests.post(
            f"{API_V1}/api-keys",
            headers=api_headers,
            json={"name": "Test Key", "scopes": ["read"]},
        )

        if response.status_code == 201:
            data = response.json()
            # Key should only be shown once on creation
            assert "key" in data

            # Subsequent GET should not show the key
            key_id = data.get("id")
            get_response = requests.get(
                f"{API_V1}/api-keys/{key_id}", headers=api_headers
            )
            if get_response.status_code == 200:
                key_data = get_response.json()
                assert "key" not in key_data or key_data.get("key") is None

    def test_invalid_api_key_format(self):
        """Test handling of invalid API key formats"""
        invalid_keys = [
            "not-a-valid-key",
            "mk_short",
            "mk_" + "a" * 100,
            "prefix_" + "b" * 32,
        ]

        for key in invalid_keys:
            headers = {"X-API-Key": key}
            response = requests.get(f"{API_V1}/scenarios", headers=headers)
            assert response.status_code in [401, 403]

    # ============================================
    # ERROR HANDLING TESTS
    # ============================================

    def test_error_message_leakage(self):
        """Test that error messages don't leak sensitive information"""
        response = requests.post(
            f"{API_V1}/auth/login", json={"username": "test", "password": "test"}
        )

        if response.status_code != 200:
            response_text = response.text.lower()
            # Should not expose internal details
            assert "sql" not in response_text
            assert "database" not in response_text
            assert "exception" not in response_text
            assert "stack trace" not in response_text

    def test_verbose_error_in_production(self):
        """Test that production doesn't show verbose errors"""
        # Trigger a 404
        response = requests.get(f"{BASE_URL}/nonexistent-endpoint-that-doesnt-exist")

        if response.status_code == 404:
            # Should be generic message, not framework-specific
            assert len(response.text) < 500  # Not a full stack trace

    # ============================================
    # INFORMATION DISCLOSURE TESTS
    # ============================================

    def test_information_disclosure_in_headers(self):
        """Test that headers don't leak sensitive information"""
        response = requests.get(f"{BASE_URL}/health")

        server_header = response.headers.get("Server", "")
        powered_by = response.headers.get("X-Powered-By", "")

        # Should not reveal specific versions
        assert "fastapi" not in server_header.lower()
        assert "uvicorn" not in server_header.lower()
        assert "python" not in powered_by.lower()

    def test_stack_trace_disclosure(self):
        """Test that stack traces are not exposed"""
        # Try to trigger an error
        response = requests.get(f"{API_V1}/scenarios/invalid-uuid-format")

        response_text = response.text.lower()
        assert "traceback" not in response_text
        assert 'file "' not in response_text
        assert ".py" not in response_text or response.status_code != 500
