"""
InsAIts SDK - Security Tests
============================
Tests for SOC 2 compliance: input validation, SSRF protection,
path traversal prevention, credential masking, encrypted cache,
PBKDF2 hashing, and audit logging.
"""

import json
import os
import tempfile
from pathlib import Path

import pytest

from insa_its.security import (
    validate_agent_id,
    validate_message_text,
    validate_url,
    validate_api_key_format,
    sanitize_path,
    mask_credential,
    hash_api_key,
    verify_api_key_hash,
    hmac_compare,
    safe_json_loads,
    set_secure_file_permissions,
    AuditLogger,
    CRYPTO_AVAILABLE,
)


# ============================================
# Input Validation Tests
# ============================================

class TestValidateAgentId:
    def test_valid_ids(self):
        assert validate_agent_id("agent-1") == "agent-1"
        assert validate_agent_id("gpt-4") == "gpt-4"
        assert validate_agent_id("user@domain.com") == "user@domain.com"
        assert validate_agent_id("agent:model:v2") == "agent:model:v2"
        assert validate_agent_id("a_b_c") == "a_b_c"

    def test_rejects_empty(self):
        with pytest.raises(ValueError):
            validate_agent_id("")

    def test_rejects_too_long(self):
        with pytest.raises(ValueError):
            validate_agent_id("a" * 256)

    def test_rejects_path_traversal(self):
        with pytest.raises(ValueError):
            validate_agent_id("../../etc/passwd")

    def test_rejects_null_bytes(self):
        with pytest.raises(ValueError):
            validate_agent_id("agent\x00id")

    def test_rejects_spaces(self):
        with pytest.raises(ValueError):
            validate_agent_id("agent id")

    def test_rejects_sql_injection(self):
        with pytest.raises(ValueError):
            validate_agent_id("'; DROP TABLE users;--")

    def test_rejects_non_string(self):
        with pytest.raises(ValueError):
            validate_agent_id(123)


class TestValidateMessageText:
    def test_valid_text(self):
        assert validate_message_text("Hello world") == "Hello world"

    def test_preserves_newlines_tabs(self):
        text = "Line 1\nLine 2\tTabbed"
        assert validate_message_text(text) == text

    def test_strips_null_bytes(self):
        result = validate_message_text("Hello\x00World")
        assert "\x00" not in result
        assert result == "HelloWorld"

    def test_strips_control_chars(self):
        result = validate_message_text("Hello\x01\x02World")
        assert result == "HelloWorld"

    def test_rejects_over_max_length(self):
        with pytest.raises(ValueError):
            validate_message_text("x" * 1_000_001)

    def test_custom_max_length(self):
        with pytest.raises(ValueError):
            validate_message_text("x" * 101, max_length=100)

    def test_rejects_non_string(self):
        with pytest.raises(ValueError):
            validate_message_text(42)


class TestValidateApiKeyFormat:
    def test_valid_keys(self):
        assert validate_api_key_format("sk-abc123def456") is True
        assert validate_api_key_format("test_key_12345678") is True

    def test_rejects_too_short(self):
        assert validate_api_key_format("short") is False

    def test_rejects_too_long(self):
        assert validate_api_key_format("x" * 129) is False

    def test_rejects_special_chars(self):
        assert validate_api_key_format("key with spaces") is False
        assert validate_api_key_format("key;injection") is False

    def test_rejects_non_string(self):
        assert validate_api_key_format(None) is False
        assert validate_api_key_format(123) is False


# ============================================
# SSRF Protection Tests
# ============================================

class TestValidateUrl:
    def test_valid_https(self):
        assert validate_url("https://api.example.com/webhook") == "https://api.example.com/webhook"

    def test_blocks_http_external(self):
        with pytest.raises(ValueError, match="HTTPS"):
            validate_url("http://api.example.com/webhook")

    def test_blocks_localhost(self):
        with pytest.raises(ValueError, match="private"):
            validate_url("http://localhost:8080/api")

    def test_blocks_private_10x(self):
        with pytest.raises(ValueError, match="private"):
            validate_url("http://10.0.0.1/internal")

    def test_blocks_private_172(self):
        with pytest.raises(ValueError, match="private"):
            validate_url("http://172.16.0.1/internal")

    def test_blocks_private_192(self):
        with pytest.raises(ValueError, match="private"):
            validate_url("http://192.168.1.1/internal")

    def test_blocks_aws_metadata(self):
        with pytest.raises(ValueError, match="private"):
            validate_url("http://169.254.169.254/latest/meta-data/")

    def test_blocks_ipv6_loopback(self):
        with pytest.raises(ValueError, match="private"):
            validate_url("http://[::1]/api")

    def test_allows_localhost_when_flag_set(self):
        result = validate_url("http://localhost:11434/api", allow_localhost=True)
        assert "localhost" in result

    def test_blocks_file_scheme(self):
        with pytest.raises(ValueError, match="scheme"):
            validate_url("file:///etc/passwd")

    def test_blocks_ftp_scheme(self):
        with pytest.raises(ValueError, match="scheme"):
            validate_url("ftp://example.com/file")

    def test_blocks_empty_url(self):
        with pytest.raises(ValueError):
            validate_url("")

    def test_blocks_no_hostname(self):
        with pytest.raises(ValueError):
            validate_url("https://")


# ============================================
# Path Traversal Tests
# ============================================

class TestSanitizePath:
    def test_valid_filename(self):
        base = Path(tempfile.gettempdir()).resolve()
        result = sanitize_path("report.csv", base)
        assert result.parent == base
        assert result.name == "report.csv"

    def test_blocks_parent_traversal(self):
        base = Path(tempfile.gettempdir()).resolve() / "exports"
        base.mkdir(exist_ok=True)
        # sanitize_path extracts only the filename component
        result = sanitize_path("../../etc/passwd", base)
        assert result.name == "passwd"
        assert str(base) in str(result)

    def test_blocks_absolute_path(self):
        base = Path(tempfile.gettempdir()).resolve() / "exports"
        base.mkdir(exist_ok=True)
        # Should extract just the filename
        result = sanitize_path("/etc/passwd", base)
        assert result.name == "passwd"
        assert str(base) in str(result)

    def test_rejects_dot(self):
        base = Path(tempfile.gettempdir())
        with pytest.raises(ValueError):
            sanitize_path(".", base)

    def test_rejects_dotdot(self):
        base = Path(tempfile.gettempdir())
        with pytest.raises(ValueError):
            sanitize_path("..", base)

    def test_rejects_empty(self):
        base = Path(tempfile.gettempdir())
        with pytest.raises(ValueError):
            sanitize_path("", base)


# ============================================
# Credential Masking Tests
# ============================================

class TestMaskCredential:
    def test_masks_long_key(self):
        result = mask_credential("sk-abc123xyz789")
        assert result.startswith("sk-a")
        assert result.endswith("z789")
        assert "***" in result
        # Original key should not be fully visible
        assert result != "sk-abc123xyz789"

    def test_masks_short_key(self):
        result = mask_credential("short")
        assert "***" not in result or len(result) == len("short")
        # Should still be masked
        assert result != "short" or len("short") <= 8

    def test_empty_string(self):
        assert mask_credential("") == "***"

    def test_very_short(self):
        result = mask_credential("ab")
        assert result == "**"


# ============================================
# PBKDF2 Hashing Tests
# ============================================

class TestHashApiKey:
    def test_produces_hash(self):
        key_hash, salt = hash_api_key("test-key-12345678")
        assert len(key_hash) > 0
        assert len(salt) > 0

    def test_different_salt_different_hash(self):
        hash1, salt1 = hash_api_key("test-key-12345678")
        hash2, salt2 = hash_api_key("test-key-12345678")
        # Random salt means different hash each time
        assert salt1 != salt2
        assert hash1 != hash2

    def test_same_salt_same_hash(self):
        _, salt = hash_api_key("test-key-12345678")
        hash1, _ = hash_api_key("test-key-12345678", salt=salt)
        hash2, _ = hash_api_key("test-key-12345678", salt=salt)
        assert hash1 == hash2

    def test_anonymous_key(self):
        key_hash, salt = hash_api_key(None)
        assert key_hash == "anonymous"

    def test_verify_correct_key(self):
        key_hash, salt = hash_api_key("my-secret-key-123456")
        assert verify_api_key_hash("my-secret-key-123456", key_hash, salt) is True

    def test_verify_wrong_key(self):
        key_hash, salt = hash_api_key("my-secret-key-123456")
        assert verify_api_key_hash("wrong-key-12345678", key_hash, salt) is False


# ============================================
# Constant-Time Comparison Tests
# ============================================

class TestHmacCompare:
    def test_equal_strings(self):
        assert hmac_compare("abc", "abc") is True

    def test_unequal_strings(self):
        assert hmac_compare("abc", "def") is False

    def test_different_lengths(self):
        assert hmac_compare("abc", "abcd") is False


# ============================================
# Safe JSON Parsing Tests
# ============================================

class TestSafeJsonLoads:
    def test_valid_json(self):
        result = safe_json_loads('{"key": "value"}')
        assert result == {"key": "value"}

    def test_rejects_too_large(self):
        result = safe_json_loads("x" * 60000)
        assert result is None

    def test_rejects_non_dict(self):
        result = safe_json_loads('[1, 2, 3]')
        assert result is None

    def test_rejects_invalid_json(self):
        result = safe_json_loads("not json at all")
        assert result is None

    def test_rejects_deeply_nested(self):
        # Build deeply nested dict
        nested = '{"a": ' * 15 + '1' + '}' * 15
        result = safe_json_loads(nested, max_depth=10)
        assert result is None

    def test_rejects_non_string(self):
        result = safe_json_loads(42)
        assert result is None


# ============================================
# Audit Logger Tests
# ============================================

class TestAuditLogger:
    def test_creates_logger(self):
        logger = AuditLogger(session_id="test-session")
        assert logger.session_id == "test-session"

    def test_disabled_logger_does_nothing(self):
        logger = AuditLogger(enabled=False)
        # Should not raise
        logger.log("test_event", {"key": "value"})
        logger.auth_success("free")
        logger.auth_failure("bad key")

    def test_log_events(self):
        logger = AuditLogger(session_id="test")
        # These should not raise
        logger.auth_success("lifetime")
        logger.auth_failure("expired")
        logger.rate_limit_exceeded("agent-1")
        logger.export_performed("csv", "/tmp/export.csv")
        logger.validation_error("agent_id", "invalid chars")
        logger.ssrf_blocked("http://169.254.169.254/")
        logger.anomaly_detected("SHORTHAND_EMERGENCE", "high")


# ============================================
# Integration Smoke Test
# ============================================

class TestSecurityIntegration:
    def test_import_chain(self):
        """Verify security module imports work across the SDK."""
        from insa_its.security import validate_agent_id
        from insa_its.config import INSAITS_CACHE_DIR_NAME, ENCRYPTION_KEY_FILENAME
        from insa_its.exceptions import ValidationError, SecurityError, SSRFError

        assert INSAITS_CACHE_DIR_NAME == ".insaits"
        assert ENCRYPTION_KEY_FILENAME == ".encryption_key"
        assert issubclass(SSRFError, SecurityError)
        assert issubclass(SecurityError, Exception)
