"""
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  # noqa: F401
import os  # noqa: F401
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,  # noqa: F401
    AuditLogger,
    CRYPTO_AVAILABLE,  # noqa: F401
)


# ============================================
# 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  # noqa: F401
        from insa_its.config import INSAITS_CACHE_DIR_NAME, ENCRYPTION_KEY_FILENAME
        from insa_its.exceptions import ValidationError, SecurityError, SSRFError  # noqa: F401

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