"""
Tests for SQLiteCatalogStore — CRUD, upsert idempotency, prefix matching, sync history.
"""

import uuid
from datetime import datetime, timedelta, timezone

import pytest

from autopil.db import create_catalog_store
from autopil.models import CatalogClassification, CatalogConnection, SyncResult


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

TENANT = "test_tenant"


def _make_connection(tenant_id: str = TENANT, **overrides) -> CatalogConnection:
    now = datetime.utcnow()
    defaults = dict(
        connection_id=str(uuid.uuid4()),
        tenant_id=tenant_id,
        catalog_type="unity_catalog",
        display_name="Test UC",
        base_url="https://test.azuredatabricks.net",
        credentials_enc='{"host": "test"}',
        sync_interval_minutes=15,
        is_enabled=True,
        created_at=now,
        updated_at=now,
    )
    defaults.update(overrides)
    return CatalogConnection(**defaults)


def _make_classification(connection_id: str, asset_fqn: str, **overrides) -> CatalogClassification:
    now = datetime.utcnow()
    defaults = dict(
        classification_id=str(uuid.uuid4()),
        connection_id=connection_id,
        tenant_id=TENANT,
        asset_fqn=asset_fqn,
        asset_type="table",
        sensitivity_label="high",
        raw_label="Confidential",
        data_classes=["PII"],
        is_certified=True,
        trust_score=0.9,
        catalog_policy_ids=[],
        fetched_at=now,
        expires_at=now + timedelta(hours=1),
    )
    defaults.update(overrides)
    return CatalogClassification(**defaults)


# ── fixtures ──────────────────────────────────────────────────────────────────

@pytest.fixture
def store(tmp_path):
    return create_catalog_store(db_path=str(tmp_path / "catalog.db"))


@pytest.fixture
def connection(store) -> CatalogConnection:
    conn = _make_connection()
    store.create_connection(conn)
    return conn


# ── connection CRUD ───────────────────────────────────────────────────────────

class TestConnectionCRUD:

    def test_create_and_get(self, store):
        conn = _make_connection()
        store.create_connection(conn)
        fetched = store.get_connection(conn.connection_id, TENANT)
        assert fetched is not None
        assert fetched.connection_id == conn.connection_id
        assert fetched.catalog_type == "unity_catalog"

    def test_get_missing_returns_none(self, store):
        assert store.get_connection("nonexistent", TENANT) is None

    def test_list_connections(self, store):
        c1 = _make_connection()
        c2 = _make_connection()
        store.create_connection(c1)
        store.create_connection(c2)
        results = store.list_connections(TENANT)
        ids = {r.connection_id for r in results}
        assert c1.connection_id in ids
        assert c2.connection_id in ids

    def test_list_returns_only_own_tenant(self, store):
        c1 = _make_connection(tenant_id=TENANT)
        c2 = _make_connection(tenant_id="other_tenant")
        store.create_connection(c1)
        store.create_connection(c2)
        results = store.list_connections(TENANT)
        assert all(r.tenant_id == TENANT for r in results)

    def test_update_connection(self, store, connection):
        updated = store.update_connection(
            connection.connection_id,
            {"display_name": "Updated Name", "sync_interval_minutes": 30},
            TENANT,
        )
        assert updated is True
        fetched = store.get_connection(connection.connection_id, TENANT)
        assert fetched.display_name == "Updated Name"
        assert fetched.sync_interval_minutes == 30

    def test_update_nonexistent_returns_false(self, store):
        assert store.update_connection("bad-id", {"display_name": "x"}, TENANT) is False

    def test_delete_connection(self, store, connection):
        deleted = store.delete_connection(connection.connection_id, TENANT)
        assert deleted is True
        assert store.get_connection(connection.connection_id, TENANT) is None

    def test_delete_nonexistent_returns_false(self, store):
        assert store.delete_connection("bad-id", TENANT) is False

    def test_mark_sync_completed(self, store, connection):
        sync_time = datetime.utcnow()
        store.mark_sync_completed(connection.connection_id, sync_time)
        fetched = store.get_connection(connection.connection_id, TENANT)
        assert fetched.last_sync_at is not None


# ── classification upsert + lookup ────────────────────────────────────────────

class TestClassifications:

    def test_upsert_and_list(self, store, connection):
        cls = _make_classification(connection.connection_id, "catalog.schema.table1")
        count = store.upsert_classifications([cls])
        assert count == 1
        results = store.list_classifications(TENANT)
        assert len(results) == 1

    def test_upsert_is_idempotent(self, store, connection):
        cls = _make_classification(connection.connection_id, "catalog.schema.table1")
        store.upsert_classifications([cls])
        # upsert same asset_fqn again — no duplicate
        store.upsert_classifications([cls])
        results = store.list_classifications(TENANT)
        assert len(results) == 1

    def test_upsert_updates_existing(self, store, connection):
        cls = _make_classification(
            connection.connection_id, "catalog.schema.table1",
            sensitivity_label="medium"
        )
        store.upsert_classifications([cls])
        # Re-upsert with higher sensitivity
        cls2 = _make_classification(
            connection.connection_id, "catalog.schema.table1",
            sensitivity_label="restricted"
        )
        store.upsert_classifications([cls2])
        results = store.list_classifications(TENANT)
        assert len(results) == 1
        assert results[0].sensitivity_label == "restricted"

    def test_exact_asset_lookup(self, store, connection):
        cls = _make_classification(connection.connection_id, "catalog.schema.table1")
        store.upsert_classifications([cls])
        results = store.get_classifications_for_asset(TENANT, "catalog.schema.table1")
        assert len(results) == 1
        assert results[0].asset_fqn == "catalog.schema.table1"

    def test_prefix_asset_lookup(self, store, connection):
        """A request for 'catalog.schema.table1' should match column-level fqns that start with it."""
        cls = _make_classification(
            connection.connection_id,
            "catalog.schema.table1.ssn_column",
            asset_type="column",
        )
        store.upsert_classifications([cls])
        # Source_id "catalog.schema.table1" should match the column-level entry
        results = store.get_classifications_for_asset(TENANT, "catalog.schema.table1")
        assert len(results) == 1

    def test_no_match_returns_empty(self, store, connection):
        cls = _make_classification(connection.connection_id, "catalog.schema.other_table")
        store.upsert_classifications([cls])
        results = store.get_classifications_for_asset(TENANT, "catalog.schema.table1")
        assert results == []

    def test_list_classifications_filter_by_connection(self, store, connection):
        other_conn = _make_connection()
        store.create_connection(other_conn)

        cls1 = _make_classification(connection.connection_id, "catalog.schema.t1")
        cls2 = _make_classification(other_conn.connection_id, "catalog.schema.t2")
        store.upsert_classifications([cls1, cls2])

        results = store.list_classifications(TENANT, connection_id=connection.connection_id)
        assert len(results) == 1
        assert results[0].asset_fqn == "catalog.schema.t1"

    def test_delete_stale_removes_expired(self, store, connection):
        past = datetime.utcnow() - timedelta(hours=2)
        cls = _make_classification(
            connection.connection_id, "catalog.schema.stale_table",
            expires_at=past,
        )
        store.upsert_classifications([cls])
        removed = store.delete_stale_classifications(connection.connection_id)
        assert removed >= 1
        results = store.list_classifications(TENANT)
        assert results == []

    def test_delete_stale_keeps_fresh(self, store, connection):
        cls = _make_classification(
            connection.connection_id, "catalog.schema.fresh_table",
            expires_at=datetime.utcnow() + timedelta(hours=2),
        )
        store.upsert_classifications([cls])
        removed = store.delete_stale_classifications(connection.connection_id)
        assert removed == 0
        results = store.list_classifications(TENANT)
        assert len(results) == 1


# ── sync history ─────────────────────────────────────────────────────────────

class TestSyncHistory:

    def _make_result(self, connection_id: str, status: str = "success") -> SyncResult:
        now = datetime.utcnow()
        return SyncResult(
            sync_id=str(uuid.uuid4()),
            connection_id=connection_id,
            tenant_id=TENANT,
            status=status,
            assets_synced=10,
            assets_skipped=0,
            started_at=now,
            completed_at=now,
        )

    def test_record_and_list(self, store, connection):
        result = self._make_result(connection.connection_id)
        store.record_sync(result)
        history = store.list_sync_history(connection.connection_id, TENANT)
        assert len(history) == 1
        assert history[0].sync_id == result.sync_id
        assert history[0].status == "success"

    def test_multiple_syncs_ordered_desc(self, store, connection):
        for _ in range(3):
            store.record_sync(self._make_result(connection.connection_id))
        history = store.list_sync_history(connection.connection_id, TENANT)
        assert len(history) == 3

    def test_failed_sync_recorded(self, store, connection):
        result = self._make_result(connection.connection_id, status="failed")
        result.error_message = "Connection timeout"
        store.record_sync(result)
        history = store.list_sync_history(connection.connection_id, TENANT)
        assert history[0].status == "failed"
        assert history[0].error_message == "Connection timeout"
