"""
Tests for ClassificationEnricher.

Coverage:
  - No catalog hit → request passes through unchanged
  - Catalog hit with higher sensitivity → sensitivity upgraded
  - Catalog hit with equal/lower sensitivity → no change (never downgrades)
  - Expired classification → still returned until delete_stale is called (store concern)
  - Store error → fail-open, request unchanged
  - Most-restrictive wins when multiple classifications match
  - write_back fires without blocking (daemon thread, no exception)
"""

import uuid
from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch

import pytest

from autopil.catalog.enricher import ClassificationEnricher, _most_restrictive
from autopil.models import (
    CatalogClassification, ContextRequest, SensitivityLevel,
)


# ── helpers ───────────────────────────────────────────────────────────────────

def _make_request(source_id: str = "catalog.schema.table1", sensitivity: str = "low") -> ContextRequest:
    return ContextRequest(
        query="SELECT * FROM customers",
        agent_role="analyst",
        user_id="u1",
        source_id=source_id,
        sensitivity_level=SensitivityLevel(sensitivity),
        session_id=str(uuid.uuid4()),
    )


def _make_classification(sensitivity_label: str, asset_fqn: str = "catalog.schema.table1") -> CatalogClassification:
    now = datetime.utcnow()
    return CatalogClassification(
        classification_id=str(uuid.uuid4()),
        connection_id="conn1",
        tenant_id="test_tenant",
        asset_fqn=asset_fqn,
        asset_type="table",
        sensitivity_label=sensitivity_label,
        raw_label=sensitivity_label.upper(),
        data_classes=["PII"] if sensitivity_label in ("high", "restricted") else [],
        is_certified=True,
        trust_score=None,
        catalog_policy_ids=[],
        fetched_at=now,
        expires_at=now + timedelta(hours=1),
    )


def _enricher(classifications: list) -> tuple[ClassificationEnricher, MagicMock]:
    """Return (enricher, mock_store) with get_classifications_for_asset pre-configured."""
    store = MagicMock()
    store.get_classifications_for_asset.return_value = classifications
    store.list_connections.return_value = []
    return ClassificationEnricher(store), store


# ── enrich: pass-through cases ────────────────────────────────────────────────

class TestEnrichPassThrough:

    def test_no_classifications_returns_original(self):
        enricher, _ = _enricher([])
        req = _make_request(sensitivity="medium")
        result = enricher.enrich(req, "test_tenant")
        assert result is req  # same object, not a copy

    def test_store_error_fail_open(self):
        store = MagicMock()
        store.get_classifications_for_asset.side_effect = Exception("DB down")
        enricher = ClassificationEnricher(store)
        req = _make_request(sensitivity="medium")
        # Must not raise; returns original request
        result = enricher.enrich(req, "test_tenant")
        assert result is req

    def test_catalog_same_sensitivity_no_copy(self):
        cls = _make_classification("medium")
        enricher, _ = _enricher([cls])
        req = _make_request(sensitivity="medium")
        result = enricher.enrich(req, "test_tenant")
        assert result is req

    def test_catalog_lower_sensitivity_no_downgrade(self):
        """Catalog says 'low' but caller says 'high' — must stay 'high'."""
        cls = _make_classification("low")
        enricher, _ = _enricher([cls])
        req = _make_request(sensitivity="high")
        result = enricher.enrich(req, "test_tenant")
        assert result is req
        assert result.sensitivity_level == SensitivityLevel.HIGH


# ── enrich: upgrade cases ─────────────────────────────────────────────────────

class TestEnrichUpgrade:

    def test_low_to_high(self):
        cls = _make_classification("high")
        enricher, _ = _enricher([cls])
        req = _make_request(sensitivity="low")
        result = enricher.enrich(req, "test_tenant")
        assert result is not req
        assert result.sensitivity_level == SensitivityLevel.HIGH

    def test_medium_to_restricted(self):
        cls = _make_classification("restricted")
        enricher, _ = _enricher([cls])
        req = _make_request(sensitivity="medium")
        result = enricher.enrich(req, "test_tenant")
        assert result.sensitivity_level == SensitivityLevel.RESTRICTED

    def test_upgraded_request_preserves_other_fields(self):
        cls = _make_classification("high")
        enricher, _ = _enricher([cls])
        req = _make_request(source_id="catalog.schema.customers", sensitivity="low")
        result = enricher.enrich(req, "test_tenant")
        assert result.query == req.query
        assert result.agent_role == req.agent_role
        assert result.user_id == req.user_id
        assert result.source_id == req.source_id
        assert result.session_id == req.session_id

    def test_upgraded_request_has_data_classes(self):
        cls = _make_classification("high")
        cls.data_classes = ["PII", "Financial"]
        enricher, _ = _enricher([cls])
        req = _make_request(sensitivity="low")
        result = enricher.enrich(req, "test_tenant")
        assert result.catalog_data_classes == ["PII", "Financial"]

    def test_most_restrictive_wins(self):
        """Multiple classifications — most restrictive label should win."""
        classes = [
            _make_classification("low"),
            _make_classification("restricted"),
            _make_classification("medium"),
        ]
        enricher, _ = _enricher(classes)
        req = _make_request(sensitivity="low")
        result = enricher.enrich(req, "test_tenant")
        assert result.sensitivity_level == SensitivityLevel.RESTRICTED


# ── _most_restrictive helper ──────────────────────────────────────────────────

class TestMostRestrictive:

    def test_picks_restricted_over_high(self):
        cls_high = _make_classification("high")
        cls_restricted = _make_classification("restricted")
        best = _most_restrictive([cls_high, cls_restricted])
        assert best.sensitivity_label == "restricted"

    def test_single_item(self):
        cls = _make_classification("medium")
        assert _most_restrictive([cls]) is cls

    def test_unknown_label_treated_as_low(self):
        cls_unknown = _make_classification("unknown_label")
        cls_medium = _make_classification("medium")
        best = _most_restrictive([cls_unknown, cls_medium])
        assert best.sensitivity_label == "medium"


# ── write_back ────────────────────────────────────────────────────────────────

class TestWriteBack:

    def test_write_back_disabled_does_nothing(self):
        store = MagicMock()
        store.list_connections.return_value = []
        enricher = ClassificationEnricher(store, write_back_enabled=False)
        from autopil.models import AuditEvent, Decision
        event = AuditEvent(
            event_id=str(uuid.uuid4()),
            session_id=str(uuid.uuid4()),
            agent_role="analyst",
            user_id="u1",
            source_id="catalog.schema.table1",
            query="q",
            decision=Decision.ALLOW,
            policy_name="test_policy",
            reason="allowed",
            timestamp=datetime.utcnow(),
        )
        enricher.write_back(event, "test_tenant")
        store.list_connections.assert_not_called()

    def test_write_back_enabled_spawns_thread(self):
        """write_back should spawn a daemon thread and return immediately."""
        store = MagicMock()
        store.list_connections.return_value = []
        enricher = ClassificationEnricher(store, write_back_enabled=True)
        from autopil.models import AuditEvent, Decision
        event = AuditEvent(
            event_id=str(uuid.uuid4()),
            session_id=str(uuid.uuid4()),
            agent_role="analyst",
            user_id="u1",
            source_id="catalog.schema.table1",
            query="q",
            decision=Decision.ALLOW,
            policy_name="test_policy",
            reason="allowed",
            timestamp=datetime.utcnow(),
        )
        import threading
        threads_before = threading.active_count()
        enricher.write_back(event, "test_tenant")
        # Does not raise, does not block
