"""Tests for the privacy classification and PII detection module."""

from __future__ import annotations

from datetime import datetime  # noqa: F401 -- Pydantic forward ref resolution

import pytest

from llmhosts.privacy.detector import PIIDetector, PIIType
from llmhosts.privacy.models import PrivacyClassification, PrivacySettings, PrivacyTier
from llmhosts.proxy.models import UnifiedMessage, UnifiedRequest
from llmhosts.router.engine import Router
from llmhosts.router.models import BackendInfo

BackendInfo.model_rebuild()


# -------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------


def _make_request(model: str = "llama3.2:8b", content: str = "Hello") -> UnifiedRequest:
    return UnifiedRequest(
        model=model,
        messages=[UnifiedMessage(role="user", content=content)],
    )


def _ollama_backend(models: list[str] | None = None) -> BackendInfo:
    return BackendInfo(
        backend_type="ollama",
        url="http://127.0.0.1:11434",
        models=models or ["llama3.2:8b", "mistral:7b"],
        is_local=True,
        is_healthy=True,
    )


def _openai_backend() -> BackendInfo:
    return BackendInfo(
        backend_type="openai",
        url="https://api.openai.com",
        models=["gpt-4o", "gpt-4o-mini"],
        is_local=False,
        is_healthy=True,
    )


# ===================================================================
# PIIDetector -- basic pattern detection
# ===================================================================


class TestPIIDetector:
    """Tests for the PIIDetector regex-based PII scanner."""

    def test_no_pii_returns_empty(self) -> None:
        detector = PIIDetector()
        matches = detector.scan("Hello, world! This is a test message.")
        assert matches == []

    def test_email_detected(self) -> None:
        detector = PIIDetector()
        matches = detector.scan("Contact me at user@example.com please")
        assert len(matches) == 1
        assert matches[0].pii_type == PIIType.EMAIL
        assert matches[0].tier == PrivacyTier.PRIVATE

    def test_multiple_emails(self) -> None:
        detector = PIIDetector()
        matches = detector.scan("Send to alice@test.com and bob@company.org")
        email_matches = [m for m in matches if m.pii_type == PIIType.EMAIL]
        assert len(email_matches) == 2

    def test_ssn_detected(self) -> None:
        detector = PIIDetector()
        matches = detector.scan("My SSN is 123-45-6789")
        ssn_matches = [m for m in matches if m.pii_type == PIIType.SSN]
        assert len(ssn_matches) == 1
        assert ssn_matches[0].tier == PrivacyTier.SENSITIVE

    def test_ssn_with_spaces(self) -> None:
        detector = PIIDetector()
        matches = detector.scan("SSN: 123 45 6789")
        ssn_matches = [m for m in matches if m.pii_type == PIIType.SSN]
        assert len(ssn_matches) == 1

    def test_invalid_ssn_ignored(self) -> None:
        """SSNs starting with 000 or 666 are invalid."""
        detector = PIIDetector()
        matches = detector.scan("Number 000-12-3456")
        ssn_matches = [m for m in matches if m.pii_type == PIIType.SSN]
        assert len(ssn_matches) == 0

    def test_credit_card_visa(self) -> None:
        detector = PIIDetector()
        matches = detector.scan("Card: 4111-1111-1111-1111")
        cc_matches = [m for m in matches if m.pii_type == PIIType.CREDIT_CARD]
        assert len(cc_matches) == 1
        assert cc_matches[0].tier == PrivacyTier.SENSITIVE

    def test_credit_card_mastercard(self) -> None:
        detector = PIIDetector()
        matches = detector.scan("MC: 5500 0000 0000 0004")
        cc_matches = [m for m in matches if m.pii_type == PIIType.CREDIT_CARD]
        assert len(cc_matches) == 1

    def test_credit_card_amex(self) -> None:
        detector = PIIDetector()
        matches = detector.scan("Amex: 3782-822463-10005")
        cc_matches = [m for m in matches if m.pii_type == PIIType.CREDIT_CARD]
        assert len(cc_matches) == 1

    def test_phone_us_format(self) -> None:
        detector = PIIDetector()
        matches = detector.scan("Call me at (555) 123-4567")
        phone_matches = [m for m in matches if m.pii_type == PIIType.PHONE]
        assert len(phone_matches) == 1
        assert phone_matches[0].tier == PrivacyTier.PRIVATE

    def test_phone_international(self) -> None:
        detector = PIIDetector()
        matches = detector.scan("Reach me at +1-555-123-4567")
        phone_matches = [m for m in matches if m.pii_type == PIIType.PHONE]
        assert len(phone_matches) == 1

    def test_ipv4_detected(self) -> None:
        detector = PIIDetector()
        matches = detector.scan("Server at 192.168.1.100 is down")
        ip_matches = [m for m in matches if m.pii_type == PIIType.IP_ADDRESS]
        assert len(ip_matches) == 1
        assert ip_matches[0].tier == PrivacyTier.PRIVATE

    def test_dob_detected(self) -> None:
        detector = PIIDetector()
        matches = detector.scan("Born on 01/15/1990")
        dob_matches = [m for m in matches if m.pii_type == PIIType.DATE_OF_BIRTH]
        assert len(dob_matches) == 1

    def test_empty_text(self) -> None:
        detector = PIIDetector()
        matches = detector.scan("")
        assert matches == []

    def test_mixed_pii_types(self) -> None:
        detector = PIIDetector()
        text = "Email: test@example.com, SSN: 123-45-6789, Phone: (555) 123-4567"
        matches = detector.scan(text)
        types_found = {m.pii_type for m in matches}
        assert PIIType.EMAIL in types_found
        assert PIIType.SSN in types_found
        assert PIIType.PHONE in types_found


# ===================================================================
# PIIDetector -- classify method
# ===================================================================


class TestPIIClassification:
    """Tests for the classify and classify_messages methods."""

    def test_classify_public(self) -> None:
        detector = PIIDetector()
        result = detector.classify("What is the capital of France?")
        assert result.tier == PrivacyTier.PUBLIC
        assert result.pii_count == 0
        assert result.pii_types_found == []

    def test_classify_private(self) -> None:
        detector = PIIDetector()
        result = detector.classify("Send update to user@example.com")
        assert result.tier == PrivacyTier.PRIVATE
        assert "email" in result.pii_types_found
        assert result.pii_count >= 1

    def test_classify_sensitive(self) -> None:
        detector = PIIDetector()
        result = detector.classify("Process payment for card 4111-1111-1111-1111")
        assert result.tier == PrivacyTier.SENSITIVE
        assert "credit_card" in result.pii_types_found

    def test_classify_mixed_chooses_highest_tier(self) -> None:
        """When both PRIVATE and SENSITIVE PII are found, tier should be SENSITIVE."""
        detector = PIIDetector()
        result = detector.classify("Email: test@example.com, SSN: 123-45-6789")
        assert result.tier == PrivacyTier.SENSITIVE
        assert "email" in result.pii_types_found
        assert "ssn" in result.pii_types_found

    def test_classify_messages_aggregates(self) -> None:
        detector = PIIDetector()
        messages = [
            {"content": "Hello, how are you?"},
            {"content": "My email is test@example.com"},
        ]
        result = detector.classify_messages(messages)
        assert result.tier == PrivacyTier.PRIVATE
        assert "email" in result.pii_types_found

    def test_classify_messages_all_public(self) -> None:
        detector = PIIDetector()
        messages = [
            {"content": "What is 2 + 2?"},
            {"content": "Tell me about Python programming."},
        ]
        result = detector.classify_messages(messages)
        assert result.tier == PrivacyTier.PUBLIC

    def test_classify_includes_scan_time(self) -> None:
        detector = PIIDetector()
        result = detector.classify("Some text to scan")
        assert result.scan_time_ms >= 0

    def test_classify_reasoning_includes_types(self) -> None:
        detector = PIIDetector()
        result = detector.classify("Contact: test@example.com")
        assert "email" in result.reasoning


# ===================================================================
# PrivacySettings
# ===================================================================


class TestPrivacySettings:
    """Tests for user-configurable privacy settings."""

    def test_default_settings(self) -> None:
        settings = PrivacySettings()
        assert settings.allow_cloud_routing is True
        assert settings.privacy_mode == "standard"
        assert settings.redact_logs is True

    def test_standard_mode_allows_cloud_for_public(self) -> None:
        settings = PrivacySettings()
        public = PrivacyClassification(tier=PrivacyTier.PUBLIC)
        assert settings.allows_cloud(public) is True

    def test_standard_mode_blocks_cloud_for_private(self) -> None:
        settings = PrivacySettings()
        private = PrivacyClassification(tier=PrivacyTier.PRIVATE, pii_types_found=["email"])
        assert settings.allows_cloud(private) is False

    def test_cloud_routing_disabled(self) -> None:
        settings = PrivacySettings(allow_cloud_routing=False)
        public = PrivacyClassification(tier=PrivacyTier.PUBLIC)
        assert settings.allows_cloud(public) is False

    def test_strict_mode_blocks_all_cloud(self) -> None:
        settings = PrivacySettings(privacy_mode="strict")
        public = PrivacyClassification(tier=PrivacyTier.PUBLIC)
        assert settings.allows_cloud(public) is False

    def test_paranoid_mode_blocks_all_cloud(self) -> None:
        settings = PrivacySettings(privacy_mode="paranoid")
        public = PrivacyClassification(tier=PrivacyTier.PUBLIC)
        assert settings.allows_cloud(public) is False

    def test_effective_tier_override_standard(self) -> None:
        settings = PrivacySettings(privacy_mode="standard")
        assert settings.effective_tier_override() is None

    def test_effective_tier_override_strict(self) -> None:
        settings = PrivacySettings(privacy_mode="strict")
        assert settings.effective_tier_override() == PrivacyTier.PRIVATE

    def test_effective_tier_override_paranoid(self) -> None:
        settings = PrivacySettings(privacy_mode="paranoid")
        assert settings.effective_tier_override() == PrivacyTier.SENSITIVE


# ===================================================================
# Custom PII patterns
# ===================================================================


class TestCustomPatterns:
    """Tests for user-defined custom PII patterns."""

    def test_custom_pattern_detects(self) -> None:
        detector = PIIDetector(custom_patterns={"employee_id": r"\bEMP-\d{6}\b"})
        matches = detector.scan("Assigned to EMP-123456")
        assert len(matches) >= 1

    def test_invalid_custom_pattern_ignored(self) -> None:
        """Invalid regex should be silently ignored, not crash."""
        detector = PIIDetector(custom_patterns={"bad": r"[invalid"})
        matches = detector.scan("Hello world")
        assert matches == []


# ===================================================================
# Privacy-aware routing -- Router integration
# ===================================================================


class TestPrivacyAwareRouting:
    """Tests for privacy-aware routing in the Router engine."""

    @pytest.mark.unit
    async def test_public_request_routes_to_cloud(self) -> None:
        """A request with no PII can route to any backend."""
        router = Router()
        router.register_backend(_ollama_backend())
        router.register_backend(_openai_backend())

        decision = await router.route(_make_request("gpt-4o", "What is Python?"))
        # Should route to cloud (openai) or local -- either is acceptable for PUBLIC
        assert decision.privacy_tier == "public"
        assert decision.privacy_pii_types == []

    @pytest.mark.unit
    async def test_private_request_forced_local(self) -> None:
        """A request with PII should be routed to local backend only."""
        router = Router()
        router.register_backend(_ollama_backend())
        router.register_backend(_openai_backend())

        decision = await router.route(_make_request("gpt-4o", "Send email to user@example.com about the project"))
        assert decision.privacy_tier == "private"
        assert "email" in decision.privacy_pii_types
        # Must route to local backend when PII is detected
        assert decision.backend_type == "ollama"
        assert decision.factors.get("privacy_enforced") is True

    @pytest.mark.unit
    async def test_sensitive_request_forced_local(self) -> None:
        """A request with sensitive PII should be forced to local only."""
        router = Router()
        router.register_backend(_ollama_backend())
        router.register_backend(_openai_backend())

        decision = await router.route(_make_request("gpt-4o", "Process payment for card 4111-1111-1111-1111"))
        assert decision.privacy_tier == "sensitive"
        assert "credit_card" in decision.privacy_pii_types
        assert decision.backend_type == "ollama"
        assert decision.factors.get("privacy_enforced") is True

    @pytest.mark.unit
    async def test_no_local_backend_blocks_private_request(self) -> None:
        """When PII is found but no local backend exists, the request should be blocked."""
        router = Router()
        router.register_backend(_openai_backend())  # cloud only

        decision = await router.route(_make_request("gpt-4o", "My SSN is 123-45-6789"))
        assert decision.privacy_tier == "sensitive"
        assert decision.backend_type == "none"
        assert decision.confidence == 0.0
        assert decision.factors.get("blocked") is True

    @pytest.mark.unit
    async def test_local_only_request_no_reroute_needed(self) -> None:
        """When request already routes to local AND has PII, no reroute is needed."""
        router = Router()
        router.register_backend(_ollama_backend())

        decision = await router.route(_make_request("llama3.2:8b", "Email user@example.com about this"))
        assert decision.privacy_tier == "private"
        assert decision.backend_type == "ollama"
        assert decision.factors.get("privacy_enforced") is True

    @pytest.mark.unit
    async def test_strict_mode_forces_local(self) -> None:
        """In strict mode, even PUBLIC requests are forced to local."""
        settings = PrivacySettings(privacy_mode="strict")
        router = Router(privacy_settings=settings)
        router.register_backend(_ollama_backend())
        router.register_backend(_openai_backend())

        decision = await router.route(_make_request("gpt-4o", "What is the weather today?"))
        # Even with no PII, strict mode forces local
        assert decision.backend_type == "ollama"

    @pytest.mark.unit
    async def test_privacy_tier_in_decision(self) -> None:
        """Every routing decision should include privacy_tier."""
        router = Router()
        router.register_backend(_ollama_backend())

        decision = await router.route(_make_request("llama3.2:8b", "Hello"))
        assert hasattr(decision, "privacy_tier")
        assert decision.privacy_tier in ("public", "private", "sensitive")

    @pytest.mark.unit
    async def test_pii_types_in_decision(self) -> None:
        """PII types should be listed in the routing decision."""
        router = Router()
        router.register_backend(_ollama_backend())

        decision = await router.route(_make_request("llama3.2:8b", "Contact: test@example.com, SSN: 123-45-6789"))
        assert "email" in decision.privacy_pii_types
        assert "ssn" in decision.privacy_pii_types

    @pytest.mark.unit
    async def test_update_privacy_settings(self) -> None:
        """Privacy settings can be updated at runtime."""
        router = Router()
        assert router.privacy_settings.privacy_mode == "standard"

        new_settings = PrivacySettings(privacy_mode="strict")
        router.update_privacy_settings(new_settings)
        assert router.privacy_settings.privacy_mode == "strict"

    @pytest.mark.unit
    async def test_cloud_routing_disabled_blocks_cloud(self) -> None:
        """When allow_cloud_routing is False, all requests route locally."""
        settings = PrivacySettings(allow_cloud_routing=False)
        router = Router(privacy_settings=settings)
        router.register_backend(_ollama_backend())
        router.register_backend(_openai_backend())

        decision = await router.route(_make_request("gpt-4o", "What is machine learning?"))
        assert decision.backend_type == "ollama"

    @pytest.mark.unit
    async def test_existing_tests_unaffected(self) -> None:
        """Existing routing logic still works -- prefer local, no PII = same behaviour."""
        router = Router()
        router.register_backend(_ollama_backend())
        decision = await router.route(_make_request("llama3.2:8b"))
        assert decision.backend_type == "ollama"
        assert decision.confidence > 0
        assert decision.privacy_tier == "public"


# ===================================================================
# Strict mode detector
# ===================================================================


class TestStrictModeDetector:
    """Tests for the strict (paranoid) mode detector with extended patterns."""

    def test_strict_mode_enables_extended_patterns(self) -> None:
        detector = PIIDetector(strict=True)
        # Strict mode includes IBAN and US passport patterns
        assert len(detector._patterns) > len(PIIDetector()._patterns)
