"""
Tests for autopil.catalog.credentials — encrypt/decrypt and key rotation.
"""

from __future__ import annotations

import json
import os
from unittest.mock import patch

import pytest


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

def _fernet_key() -> str:
    from cryptography.fernet import Fernet
    return Fernet.generate_key().decode()


def _make_store():
    """Temp-file SQLite catalog store for rotation tests."""
    import tempfile
    import atexit
    import os as _os
    from autopil.db import create_catalog_store
    # Cannot use :memory: — each sqlite3.connect() creates a separate in-memory db.
    f = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
    f.close()
    atexit.register(_os.unlink, f.name)
    return create_catalog_store(db_path=f.name)


def _register_connection(store, creds_enc: str, tenant_id: str = "test") -> str:
    from autopil.models import CatalogConnection
    from datetime import datetime
    import uuid
    conn_id = "conn_" + uuid.uuid4().hex[:8]
    now = datetime.utcnow()
    conn = CatalogConnection(
        connection_id=conn_id,
        tenant_id=tenant_id,
        catalog_type="unity_catalog",
        display_name="Test",
        base_url="https://test.example.com",
        credentials_enc=creds_enc,
        sync_interval_minutes=60,
        is_enabled=True,
        created_at=now,
        updated_at=now,
    )
    store.create_connection(conn)
    return conn_id


# ── encrypt / decrypt ─────────────────────────────────────────────────────────

class TestEncryptDecrypt:

    def test_plaintext_roundtrip_no_key(self):
        from autopil.catalog.credentials import encrypt_credentials, decrypt_credentials
        creds = {"token": "abc", "host": "example.com"}
        with patch.dict(os.environ, {}, clear=False):
            os.environ.pop("AUTOPIL_CATALOG_ENCRYPTION_KEY", None)
            enc = encrypt_credentials(creds)
            assert enc == json.dumps(creds)
            assert decrypt_credentials(enc) == creds

    def test_fernet_roundtrip_with_key(self):
        from autopil.catalog.credentials import encrypt_credentials, decrypt_credentials
        key = _fernet_key()
        creds = {"token": "super_secret", "host": "prod.example.com"}
        with patch.dict(os.environ, {"AUTOPIL_CATALOG_ENCRYPTION_KEY": key}):
            enc = encrypt_credentials(creds)
            assert enc != json.dumps(creds)  # must be ciphertext, not plaintext
            assert decrypt_credentials(enc) == creds

    def test_encrypted_blob_is_opaque(self):
        from autopil.catalog.credentials import encrypt_credentials
        key = _fernet_key()
        creds = {"password": "s3cr3t"}
        with patch.dict(os.environ, {"AUTOPIL_CATALOG_ENCRYPTION_KEY": key}):
            enc = encrypt_credentials(creds)
        assert "s3cr3t" not in enc

    def test_decrypt_falls_back_to_old_key(self):
        """Credential encrypted with old key should decrypt when old key is in env."""
        from autopil.catalog.credentials import encrypt_credentials, decrypt_credentials
        old_key = _fernet_key()
        new_key = _fernet_key()
        creds = {"token": "old_cred"}
        # Encrypt with old key
        with patch.dict(os.environ, {"AUTOPIL_CATALOG_ENCRYPTION_KEY": old_key}):
            enc = encrypt_credentials(creds)
        # Decrypt with new key current + old key fallback
        with patch.dict(os.environ, {
            "AUTOPIL_CATALOG_ENCRYPTION_KEY": new_key,
            "AUTOPIL_CATALOG_ENCRYPTION_KEY_OLD": old_key,
        }):
            assert decrypt_credentials(enc) == creds

    def test_decrypt_wrong_key_raises(self):
        from autopil.catalog.credentials import encrypt_credentials, decrypt_credentials
        key1 = _fernet_key()
        key2 = _fernet_key()
        creds = {"token": "abc"}
        with patch.dict(os.environ, {"AUTOPIL_CATALOG_ENCRYPTION_KEY": key1}):
            enc = encrypt_credentials(creds)
        with patch.dict(os.environ, {"AUTOPIL_CATALOG_ENCRYPTION_KEY": key2}):
            os.environ.pop("AUTOPIL_CATALOG_ENCRYPTION_KEY_OLD", None)
            with pytest.raises(ValueError):
                decrypt_credentials(enc)

    def test_bad_plaintext_raises(self):
        from autopil.catalog.credentials import decrypt_credentials
        with patch.dict(os.environ, {}, clear=False):
            os.environ.pop("AUTOPIL_CATALOG_ENCRYPTION_KEY", None)
            os.environ.pop("AUTOPIL_CATALOG_ENCRYPTION_KEY_OLD", None)
            with pytest.raises(ValueError):
                decrypt_credentials("not-json-or-fernet")


# ── key rotation ──────────────────────────────────────────────────────────────

class TestRotateEncryptionKey:

    def test_rotates_single_connection(self):
        from autopil.catalog.credentials import encrypt_credentials, decrypt_credentials, rotate_encryption_key
        store = _make_store()
        old_key = _fernet_key()
        new_key = _fernet_key()
        creds = {"token": "my_token"}

        with patch.dict(os.environ, {"AUTOPIL_CATALOG_ENCRYPTION_KEY": old_key}):
            enc_old = encrypt_credentials(creds)
        conn_id = _register_connection(store, enc_old)

        result = rotate_encryption_key(store, old_key=old_key, new_key=new_key)
        assert result["rotated"] == 1
        assert result["failed"] == 0

        # Verify new creds decrypt with new key
        updated = store.list_all_connections()[0]
        with patch.dict(os.environ, {"AUTOPIL_CATALOG_ENCRYPTION_KEY": new_key}):
            assert decrypt_credentials(updated.credentials_enc) == creds

    def test_rotates_multiple_connections(self):
        from autopil.catalog.credentials import encrypt_credentials, rotate_encryption_key
        store = _make_store()
        old_key = _fernet_key()
        new_key = _fernet_key()

        with patch.dict(os.environ, {"AUTOPIL_CATALOG_ENCRYPTION_KEY": old_key}):
            for i in range(5):
                enc = encrypt_credentials({"token": f"token_{i}"})
                _register_connection(store, enc, tenant_id=f"tenant_{i}")

        result = rotate_encryption_key(store, old_key=old_key, new_key=new_key)
        assert result["rotated"] == 5
        assert result["failed"] == 0
        assert result["total"] == 5

    def test_skips_plaintext_connections(self):
        from autopil.catalog.credentials import rotate_encryption_key
        store = _make_store()
        old_key = _fernet_key()
        new_key = _fernet_key()

        # Register with plaintext (no encryption key at registration time)
        plaintext_enc = json.dumps({"token": "plain"})
        _register_connection(store, plaintext_enc)

        result = rotate_encryption_key(store, old_key=old_key, new_key=new_key)
        assert result["skipped"] == 1
        assert result["rotated"] == 0

    def test_dry_run_makes_no_changes(self):
        from autopil.catalog.credentials import encrypt_credentials, rotate_encryption_key
        store = _make_store()
        old_key = _fernet_key()
        new_key = _fernet_key()

        with patch.dict(os.environ, {"AUTOPIL_CATALOG_ENCRYPTION_KEY": old_key}):
            enc = encrypt_credentials({"token": "dry_run_token"})
        conn_id = _register_connection(store, enc)
        original_enc = store.list_all_connections()[0].credentials_enc

        result = rotate_encryption_key(store, old_key=old_key, new_key=new_key, dry_run=True)
        assert result["dry_run"] is True
        assert result["rotated"] == 1

        # credentials_enc must be unchanged
        unchanged = store.list_all_connections()[0]
        assert unchanged.credentials_enc == original_enc

    def test_same_key_raises(self):
        from autopil.catalog.credentials import rotate_encryption_key
        store = _make_store()
        key = _fernet_key()
        with pytest.raises(ValueError, match="identical"):
            rotate_encryption_key(store, old_key=key, new_key=key)

    def test_no_new_key_raises(self):
        from autopil.catalog.credentials import rotate_encryption_key
        store = _make_store()
        with patch.dict(os.environ, {}, clear=False):
            os.environ.pop("AUTOPIL_CATALOG_ENCRYPTION_KEY", None)
            with pytest.raises(ValueError, match="No new key"):
                rotate_encryption_key(store, old_key=None, new_key=None)

    def test_reads_keys_from_env(self):
        from autopil.catalog.credentials import encrypt_credentials, decrypt_credentials, rotate_encryption_key
        store = _make_store()
        old_key = _fernet_key()
        new_key = _fernet_key()
        creds = {"token": "env_key_test"}

        with patch.dict(os.environ, {"AUTOPIL_CATALOG_ENCRYPTION_KEY": old_key}):
            enc = encrypt_credentials(creds)
        _register_connection(store, enc)

        with patch.dict(os.environ, {
            "AUTOPIL_CATALOG_ENCRYPTION_KEY": new_key,
            "AUTOPIL_CATALOG_ENCRYPTION_KEY_OLD": old_key,
        }):
            result = rotate_encryption_key(store)  # reads from env

        assert result["rotated"] == 1
        updated = store.list_all_connections()[0]
        with patch.dict(os.environ, {"AUTOPIL_CATALOG_ENCRYPTION_KEY": new_key}):
            assert decrypt_credentials(updated.credentials_enc) == creds

    def test_mixed_plaintext_and_encrypted(self):
        from autopil.catalog.credentials import encrypt_credentials, rotate_encryption_key
        store = _make_store()
        old_key = _fernet_key()
        new_key = _fernet_key()

        with patch.dict(os.environ, {"AUTOPIL_CATALOG_ENCRYPTION_KEY": old_key}):
            enc = encrypt_credentials({"token": "encrypted_one"})
        _register_connection(store, enc, tenant_id="t1")

        # Plaintext
        _register_connection(store, json.dumps({"token": "plaintext_one"}), tenant_id="t2")

        result = rotate_encryption_key(store, old_key=old_key, new_key=new_key)
        assert result["rotated"] == 1
        assert result["skipped"] == 1
        assert result["failed"] == 0
