"""
Tests for catalog REST endpoints.

Coverage:
  - POST   /v1/catalog/connections       — register (mocked test_connection)
  - GET    /v1/catalog/connections       — list
  - GET    /v1/catalog/connections/{id}  — get one; 404 on missing
  - PUT    /v1/catalog/connections/{id}  — update
  - DELETE /v1/catalog/connections/{id}  — delete
  - POST   /v1/catalog/connections/{id}/sync — immediate sync (mocked scheduler)
  - GET    /v1/catalog/connections/{id}/sync — sync history
  - GET    /v1/catalog/classifications   — list with optional filters
  - GET    /v1/catalog/classifications/lookup — by asset_fqn
  - POST   /v1/catalog/webhook/{id}      — HMAC-verified inbound push
"""

import hashlib
import hmac
import json
import uuid
from datetime import datetime, timedelta
from unittest.mock import MagicMock, patch

import pytest

from autopil.models import CatalogClassification, SyncResult


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

def _register_payload(**overrides) -> dict:
    base = {
        "catalog_type": "unity_catalog",
        "display_name": "Test UC",
        "base_url": "https://test.azuredatabricks.net",
        "credentials": {"host": "test.azuredatabricks.net", "token": "dapi_test"},
        "sync_interval_minutes": 60,
    }
    base.update(overrides)
    return base


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


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


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

@pytest.fixture
def catalog_client(client):
    """Re-use the standard authenticated TestClient; catalog endpoints require the same X-API-Key auth."""
    return client


# ── registration ─────────────────────────────────────────────────────────────

class TestRegisterConnection:

    def test_register_success(self, catalog_client):
        """test_connection passes → 201 with connection_id returned."""
        with (
            patch("autopil.catalog.registry.CatalogRegistry.is_supported", return_value=True),
            patch("autopil.catalog.registry.CatalogRegistry.get_connector") as mock_get,
        ):
            connector = MagicMock()
            connector.test_connection.return_value = (True, "")
            mock_get.return_value = connector

            r = catalog_client.post("/v1/catalog/connections", json=_register_payload())
        assert r.status_code == 200
        body = r.json()
        assert "connection_id" in body
        assert body["catalog_type"] == "unity_catalog"

    def test_register_unsupported_type(self, catalog_client):
        with patch("autopil.catalog.registry.CatalogRegistry.is_supported", return_value=False):
            r = catalog_client.post(
                "/v1/catalog/connections",
                json=_register_payload(catalog_type="unknown_catalog"),
            )
        assert r.status_code == 422

    def test_register_connection_test_fails(self, catalog_client):
        with (
            patch("autopil.catalog.registry.CatalogRegistry.is_supported", return_value=True),
            patch("autopil.catalog.registry.CatalogRegistry.get_connector") as mock_get,
        ):
            connector = MagicMock()
            connector.test_connection.return_value = (False, "invalid token")
            mock_get.return_value = connector

            r = catalog_client.post("/v1/catalog/connections", json=_register_payload())
        assert r.status_code == 422

    def test_register_requires_auth(self, client):
        from fastapi.testclient import TestClient
        from autopil.api.app import create_app
        import tempfile, textwrap
        # Client without API key header
        unauthenticated = TestClient(
            client.app,
            headers={},
        )
        r = unauthenticated.post("/v1/catalog/connections", json=_register_payload())
        assert r.status_code == 401


# ── list / get ────────────────────────────────────────────────────────────────

class TestGetConnections:

    def _register(self, catalog_client):
        with (
            patch("autopil.catalog.registry.CatalogRegistry.is_supported", return_value=True),
            patch("autopil.catalog.registry.CatalogRegistry.get_connector") as mock_get,
        ):
            connector = MagicMock()
            connector.test_connection.return_value = (True, "")
            mock_get.return_value = connector
            r = catalog_client.post("/v1/catalog/connections", json=_register_payload())
        return r.json()["connection_id"]

    def test_list_empty(self, catalog_client):
        r = catalog_client.get("/v1/catalog/connections")
        assert r.status_code == 200
        assert isinstance(r.json(), list)

    def test_list_after_register(self, catalog_client):
        conn_id = self._register(catalog_client)
        r = catalog_client.get("/v1/catalog/connections")
        assert r.status_code == 200
        ids = [c["connection_id"] for c in r.json()]
        assert conn_id in ids

    def test_get_by_id(self, catalog_client):
        conn_id = self._register(catalog_client)
        r = catalog_client.get(f"/v1/catalog/connections/{conn_id}")
        assert r.status_code == 200
        assert r.json()["connection_id"] == conn_id

    def test_get_missing_returns_404(self, catalog_client):
        r = catalog_client.get("/v1/catalog/connections/nonexistent-id")
        assert r.status_code == 404


# ── update / delete ───────────────────────────────────────────────────────────

class TestUpdateDeleteConnection:

    def _register(self, catalog_client):
        with (
            patch("autopil.catalog.registry.CatalogRegistry.is_supported", return_value=True),
            patch("autopil.catalog.registry.CatalogRegistry.get_connector") as mock_get,
        ):
            connector = MagicMock()
            connector.test_connection.return_value = (True, "")
            mock_get.return_value = connector
            r = catalog_client.post("/v1/catalog/connections", json=_register_payload())
        return r.json()["connection_id"]

    def test_update_display_name(self, catalog_client):
        conn_id = self._register(catalog_client)
        r = catalog_client.put(
            f"/v1/catalog/connections/{conn_id}",
            json={"display_name": "Renamed"},
        )
        assert r.status_code == 200
        assert r.json()["display_name"] == "Renamed"

    def test_update_missing_returns_404(self, catalog_client):
        r = catalog_client.put("/v1/catalog/connections/nonexistent", json={"display_name": "x"})
        assert r.status_code == 404

    def test_delete_connection(self, catalog_client):
        conn_id = self._register(catalog_client)
        r = catalog_client.delete(f"/v1/catalog/connections/{conn_id}")
        assert r.status_code == 200
        # Confirm it's gone
        r2 = catalog_client.get(f"/v1/catalog/connections/{conn_id}")
        assert r2.status_code == 404

    def test_delete_missing_returns_404(self, catalog_client):
        r = catalog_client.delete("/v1/catalog/connections/nonexistent")
        assert r.status_code == 404

    def test_rotate_credentials_success(self, catalog_client):
        conn_id = self._register(catalog_client)
        with (
            patch("autopil.catalog.registry.CatalogRegistry.get_connector") as mock_get,
        ):
            connector = MagicMock()
            connector.test_connection.return_value = (True, "")
            mock_get.return_value = connector
            r = catalog_client.patch(
                f"/v1/catalog/connections/{conn_id}/credentials",
                json={"credentials": {"token": "new_token"}},
            )
        assert r.status_code == 200
        assert r.json()["connection_id"] == conn_id

    def test_rotate_credentials_test_fails_returns_422(self, catalog_client):
        conn_id = self._register(catalog_client)
        with patch("autopil.catalog.registry.CatalogRegistry.get_connector") as mock_get:
            connector = MagicMock()
            connector.test_connection.return_value = (False, "bad creds")
            mock_get.return_value = connector
            r = catalog_client.patch(
                f"/v1/catalog/connections/{conn_id}/credentials",
                json={"credentials": {"token": "wrong_token"}},
            )
        assert r.status_code == 422

    def test_rotate_credentials_missing_connection_returns_404(self, catalog_client):
        r = catalog_client.patch(
            "/v1/catalog/connections/nonexistent/credentials",
            json={"credentials": {"token": "x"}},
        )
        assert r.status_code == 404


# ── immediate sync ────────────────────────────────────────────────────────────

class TestSync:

    def _register(self, catalog_client):
        with (
            patch("autopil.catalog.registry.CatalogRegistry.is_supported", return_value=True),
            patch("autopil.catalog.registry.CatalogRegistry.get_connector") as mock_get,
        ):
            connector = MagicMock()
            connector.test_connection.return_value = (True, "")
            mock_get.return_value = connector
            r = catalog_client.post("/v1/catalog/connections", json=_register_payload())
        return r.json()["connection_id"]

    def test_trigger_sync_no_scheduler_returns_503(self, catalog_client):
        """In tests the catalog scheduler is not started, so /sync returns 503."""
        conn_id = self._register(catalog_client)
        r = catalog_client.post(f"/v1/catalog/connections/{conn_id}/sync")
        assert r.status_code == 503

    def test_sync_history_empty(self, catalog_client):
        conn_id = self._register(catalog_client)
        r = catalog_client.get(f"/v1/catalog/connections/{conn_id}/sync")
        assert r.status_code == 200
        assert isinstance(r.json(), list)


# ── classifications ───────────────────────────────────────────────────────────

class TestClassifications:

    def test_list_classifications_empty(self, catalog_client):
        r = catalog_client.get("/v1/catalog/classifications")
        assert r.status_code == 200
        assert isinstance(r.json(), list)

    def test_lookup_not_found(self, catalog_client):
        r = catalog_client.get("/v1/catalog/classifications/lookup?asset_fqn=catalog.schema.missing")
        assert r.status_code == 200
        assert r.json() == []


# ── webhook ───────────────────────────────────────────────────────────────────

class TestWebhook:

    def _register(self, catalog_client, webhook_secret: str | None = None):
        payload = _register_payload()
        if webhook_secret:
            payload["webhook_secret"] = webhook_secret
        with (
            patch("autopil.catalog.registry.CatalogRegistry.is_supported", return_value=True),
            patch("autopil.catalog.registry.CatalogRegistry.get_connector") as mock_get,
        ):
            connector = MagicMock()
            connector.test_connection.return_value = (True, "")
            mock_get.return_value = connector
            r = catalog_client.post("/v1/catalog/connections", json=payload)
        return r.json()["connection_id"]

    def test_webhook_no_secret_accepts_any(self, catalog_client):
        """No webhook_secret set → accept any payload."""
        conn_id = self._register(catalog_client, webhook_secret=None)
        r = catalog_client.post(
            f"/v1/catalog/webhook/{conn_id}",
            json={"event": "classification_updated", "asset_fqn": "catalog.schema.t1"},
        )
        assert r.status_code == 200

    def test_webhook_with_valid_hmac(self, catalog_client):
        secret = "mysecret"
        conn_id = self._register(catalog_client, webhook_secret=secret)
        body = json.dumps({"event": "classification_updated"}).encode()
        sig = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
        r = catalog_client.post(
            f"/v1/catalog/webhook/{conn_id}",
            content=body,
            headers={"Content-Type": "application/json", "X-Hub-Signature-256": sig},
        )
        assert r.status_code == 200

    def test_webhook_with_invalid_hmac_rejected(self, catalog_client):
        secret = "mysecret"
        conn_id = self._register(catalog_client, webhook_secret=secret)
        body = json.dumps({"event": "classification_updated"}).encode()
        r = catalog_client.post(
            f"/v1/catalog/webhook/{conn_id}",
            content=body,
            headers={"Content-Type": "application/json", "X-Hub-Signature-256": "sha256=bad"},
        )
        assert r.status_code == 401

    def test_webhook_missing_connection_returns_404(self, catalog_client):
        r = catalog_client.post(
            "/v1/catalog/webhook/nonexistent-conn-id",
            json={"event": "test"},
        )
        assert r.status_code == 404
