"""Tests for the per-user integrations API (app/api/integrations.py)."""

import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool

from app.database import Base, get_db
from app.models import SubscriptionPlan, UserIntegration, UserProfile

# ---------------------------------------------------------------------------
# Test data constants
# ---------------------------------------------------------------------------

_OWNER = "test_user@example.com"
_OTHER_OWNER = "other_user@example.com"

_IMAP_SOURCE = {
    "direction": "SOURCE",
    "integration_type": "IMAP",
    "name": "Work Gmail",
    "config": {"host": "imap.gmail.com", "port": 993, "username": "work@example.com", "use_ssl": True},
    "credentials": {"password": "s3cr3t"},
    "is_active": True,
}

_S3_DESTINATION = {
    "direction": "DESTINATION",
    "integration_type": "S3",
    "name": "Archive Bucket",
    "config": {"bucket": "my-bucket", "region": "us-east-1"},
    "credentials": {
        "access_key_id": "AKIAIOSFODNN7EXAMPLE",
        "secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
    },
    "is_active": True,
}


# ---------------------------------------------------------------------------
# Shared fixture helpers
# ---------------------------------------------------------------------------


def _make_profile(session, owner: str = _OWNER, tier: str = "business") -> UserProfile:
    """Create a UserProfile row for the given user and tier."""
    profile = UserProfile(user_id=owner, subscription_tier=tier)
    session.add(profile)
    session.commit()
    session.refresh(profile)
    return profile


def _make_plan(
    session,
    tier: str = "business",
    max_storage_destinations: int = 10,
    max_mailboxes: int = 0,
) -> SubscriptionPlan:
    """Create a SubscriptionPlan row."""
    plan = SubscriptionPlan(
        plan_id=tier,
        name=tier.title(),
        price_monthly=7.99,
        price_yearly=76.99,
        max_storage_destinations=max_storage_destinations,
        max_mailboxes=max_mailboxes,
        is_active=True,
    )
    session.add(plan)
    session.commit()
    session.refresh(plan)
    return plan


@pytest.fixture()
def int_engine():
    """In-memory SQLite engine for integration tests."""
    engine = create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(bind=engine)
    yield engine
    Base.metadata.drop_all(bind=engine)


@pytest.fixture()
def int_session(int_engine):
    """DB session scoped to one test."""
    Session = sessionmaker(bind=int_engine)
    session = Session()
    yield session
    session.close()


def _seed_default_plan(engine, owner_id: str = _OWNER) -> None:
    """Seed a generous (business-tier) plan and profile for the given user.

    Called automatically by ``_make_client`` so that existing CRUD tests keep
    working after quota enforcement was added.
    """
    Session = sessionmaker(bind=engine)
    session = Session()
    try:
        if not session.query(SubscriptionPlan).filter(SubscriptionPlan.plan_id == "business").first():
            session.add(
                SubscriptionPlan(
                    plan_id="business",
                    name="Power",
                    price_monthly=7.99,
                    price_yearly=76.99,
                    max_storage_destinations=10,
                    max_mailboxes=0,  # 0 = unlimited for paid tiers
                    is_active=True,
                )
            )
        if not session.query(UserProfile).filter(UserProfile.user_id == owner_id).first():
            session.add(UserProfile(user_id=owner_id, subscription_tier="business"))
        session.commit()
    finally:
        session.close()


def _make_client(int_engine, owner_id: str = _OWNER):
    """Return a TestClient with *owner_id* injected as the authenticated user."""
    from app.api.integrations import _get_owner_id
    from app.main import app

    _seed_default_plan(int_engine, owner_id)

    def override_db():
        Session = sessionmaker(bind=int_engine)
        session = Session()
        try:
            yield session
        finally:
            session.close()

    def override_owner():
        return owner_id

    app.dependency_overrides[get_db] = override_db
    app.dependency_overrides[_get_owner_id] = override_owner
    with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client:
        yield client
    app.dependency_overrides.clear()


@pytest.fixture()
def int_client(int_engine):
    """TestClient authenticated as _OWNER."""
    yield from _make_client(int_engine, _OWNER)


# ---------------------------------------------------------------------------
# CRUD tests
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestCreateIntegration:
    """Tests for POST /api/integrations/."""

    def test_create_source_integration(self, int_client):
        """Create a SOURCE integration and verify the response."""
        resp = int_client.post("/api/integrations/", json=_IMAP_SOURCE)
        assert resp.status_code == 201
        data = resp.json()
        assert data["direction"] == "SOURCE"
        assert data["integration_type"] == "IMAP"
        assert data["name"] == "Work Gmail"
        assert data["config"]["host"] == "imap.gmail.com"
        assert data["has_credentials"] is True
        # Credentials must never appear in the response
        assert "credentials" not in data
        assert "password" not in data

    def test_create_destination_integration(self, int_client):
        """Create a DESTINATION integration and verify the response."""
        resp = int_client.post("/api/integrations/", json=_S3_DESTINATION)
        assert resp.status_code == 201
        data = resp.json()
        assert data["direction"] == "DESTINATION"
        assert data["integration_type"] == "S3"
        assert data["has_credentials"] is True

    def test_create_without_credentials(self, int_client):
        """Integration without credentials sets has_credentials to False."""
        payload = dict(_IMAP_SOURCE)
        payload["credentials"] = None
        resp = int_client.post("/api/integrations/", json=payload)
        assert resp.status_code == 201
        assert resp.json()["has_credentials"] is False

    def test_create_invalid_direction(self, int_client):
        """An unknown direction returns 400."""
        payload = dict(_IMAP_SOURCE, direction="INVALID")
        resp = int_client.post("/api/integrations/", json=payload)
        assert resp.status_code == 400

    def test_create_invalid_type(self, int_client):
        """An unknown integration_type returns 400."""
        payload = dict(_IMAP_SOURCE, integration_type="UNKNOWN_TYPE")
        resp = int_client.post("/api/integrations/", json=payload)
        assert resp.status_code == 400

    def test_credentials_are_encrypted_in_db(self, int_client, int_session):
        """Stored credentials must be encrypted (enc: prefix)."""
        resp = int_client.post("/api/integrations/", json=_IMAP_SOURCE)
        assert resp.status_code == 201
        rec = int_session.query(UserIntegration).first()
        assert rec is not None
        assert rec.credentials is not None
        assert rec.credentials.startswith("enc:")


@pytest.mark.integration
class TestListIntegrations:
    """Tests for GET /api/integrations/."""

    def test_list_empty(self, int_client):
        """No integrations returns empty list."""
        resp = int_client.get("/api/integrations/")
        assert resp.status_code == 200
        assert resp.json() == []

    def test_list_returns_own_records(self, int_client, int_session):
        """Users only see their own integrations."""
        # Create an integration for _OWNER via the API client
        int_client.post("/api/integrations/", json=_IMAP_SOURCE)

        # Create an integration for a different owner directly in the DB
        other_integration = UserIntegration(
            owner_id=_OTHER_OWNER,
            direction="DESTINATION",
            integration_type="S3",
            name="Archive Bucket",
            config='{"bucket": "other-bucket"}',
            is_active=True,
        )
        int_session.add(other_integration)
        int_session.commit()

        resp = int_client.get("/api/integrations/")
        assert resp.status_code == 200
        assert len(resp.json()) == 1
        assert resp.json()[0]["integration_type"] == "IMAP"

    def test_filter_by_direction(self, int_client):
        """direction query param filters results."""
        int_client.post("/api/integrations/", json=_IMAP_SOURCE)
        int_client.post("/api/integrations/", json=_S3_DESTINATION)

        resp = int_client.get("/api/integrations/?direction=SOURCE")
        assert resp.status_code == 200
        results = resp.json()
        assert len(results) == 1
        assert results[0]["direction"] == "SOURCE"

    def test_filter_by_integration_type(self, int_client):
        """integration_type query param filters results."""
        int_client.post("/api/integrations/", json=_IMAP_SOURCE)
        int_client.post("/api/integrations/", json=_S3_DESTINATION)

        resp = int_client.get("/api/integrations/?integration_type=S3")
        assert resp.status_code == 200
        results = resp.json()
        assert len(results) == 1
        assert results[0]["integration_type"] == "S3"

    def test_filter_invalid_direction_returns_400(self, int_client):
        """Unknown direction filter returns 400."""
        resp = int_client.get("/api/integrations/?direction=BAD")
        assert resp.status_code == 400

    def test_filter_invalid_type_returns_400(self, int_client):
        """Unknown integration_type filter returns 400."""
        resp = int_client.get("/api/integrations/?integration_type=BAD")
        assert resp.status_code == 400


@pytest.mark.integration
class TestGetIntegration:
    """Tests for GET /api/integrations/{id}."""

    def test_get_existing(self, int_client):
        """Retrieve a single integration by ID."""
        created = int_client.post("/api/integrations/", json=_IMAP_SOURCE).json()
        resp = int_client.get(f"/api/integrations/{created['id']}")
        assert resp.status_code == 200
        assert resp.json()["id"] == created["id"]

    def test_get_not_found(self, int_client):
        """Non-existent ID returns 404."""
        resp = int_client.get("/api/integrations/9999")
        assert resp.status_code == 404

    def test_get_other_users_integration(self, int_client, int_session):
        """Cannot retrieve another user's integration."""
        other_integration = UserIntegration(
            owner_id=_OTHER_OWNER,
            direction="SOURCE",
            integration_type="IMAP",
            name="Other Mailbox",
            is_active=True,
        )
        int_session.add(other_integration)
        int_session.commit()
        resp = int_client.get(f"/api/integrations/{other_integration.id}")
        assert resp.status_code == 404


@pytest.mark.integration
class TestUpdateIntegration:
    """Tests for PUT /api/integrations/{id}."""

    def test_update_name(self, int_client):
        """Update the name of an integration."""
        created = int_client.post("/api/integrations/", json=_IMAP_SOURCE).json()
        resp = int_client.put(f"/api/integrations/{created['id']}", json={"name": "Personal Gmail"})
        assert resp.status_code == 200
        assert resp.json()["name"] == "Personal Gmail"

    def test_update_config(self, int_client):
        """Update the config dict."""
        created = int_client.post("/api/integrations/", json=_IMAP_SOURCE).json()
        new_config = {"host": "imap.new.com", "port": 143, "username": "new@example.com", "use_ssl": False}
        resp = int_client.put(f"/api/integrations/{created['id']}", json={"config": new_config})
        assert resp.status_code == 200
        assert resp.json()["config"]["host"] == "imap.new.com"

    def test_update_credentials_re_encrypts(self, int_client, int_session):
        """Updating credentials stores the new value encrypted."""
        created = int_client.post("/api/integrations/", json=_IMAP_SOURCE).json()
        first_enc = int_session.query(UserIntegration).get(created["id"]).credentials

        resp = int_client.put(f"/api/integrations/{created['id']}", json={"credentials": {"password": "new_password"}})
        assert resp.status_code == 200
        int_session.expire_all()
        second_enc = int_session.query(UserIntegration).get(created["id"]).credentials
        # Both must be encrypted
        assert second_enc.startswith("enc:")
        # They should be different ciphertexts (Fernet uses random IV)
        assert first_enc != second_enc

    def test_update_is_active(self, int_client):
        """Deactivate an integration."""
        created = int_client.post("/api/integrations/", json=_IMAP_SOURCE).json()
        resp = int_client.put(f"/api/integrations/{created['id']}", json={"is_active": False})
        assert resp.status_code == 200
        assert resp.json()["is_active"] is False

    def test_update_not_found(self, int_client):
        """Updating a non-existent integration returns 404."""
        resp = int_client.put("/api/integrations/9999", json={"name": "Ghost"})
        assert resp.status_code == 404

    def test_update_clears_last_error(self, int_client, int_session):
        """Updating an integration resets last_error."""
        created = int_client.post("/api/integrations/", json=_IMAP_SOURCE).json()
        rec = int_session.query(UserIntegration).get(created["id"])
        rec.last_error = "Previous failure"
        int_session.commit()

        int_client.put(f"/api/integrations/{created['id']}", json={"name": "Updated"})
        int_session.expire_all()
        rec = int_session.query(UserIntegration).get(created["id"])
        assert rec.last_error is None


@pytest.mark.integration
class TestDeleteIntegration:
    """Tests for DELETE /api/integrations/{id}."""

    def test_delete_existing(self, int_client, int_session):
        """Delete an integration and confirm it is removed from the DB."""
        created = int_client.post("/api/integrations/", json=_IMAP_SOURCE).json()
        resp = int_client.delete(f"/api/integrations/{created['id']}")
        assert resp.status_code == 204
        assert int_session.query(UserIntegration).get(created["id"]) is None

    def test_delete_not_found(self, int_client):
        """Deleting a non-existent integration returns 404."""
        resp = int_client.delete("/api/integrations/9999")
        assert resp.status_code == 404

    def test_delete_other_users_integration(self, int_client, int_session):
        """Cannot delete another user's integration."""
        other_integration = UserIntegration(
            owner_id=_OTHER_OWNER,
            direction="SOURCE",
            integration_type="IMAP",
            name="Other Mailbox",
            is_active=True,
        )
        int_session.add(other_integration)
        int_session.commit()
        resp = int_client.delete(f"/api/integrations/{other_integration.id}")
        assert resp.status_code == 404


@pytest.mark.integration
class TestGetIntegrationCredentials:
    """Tests for GET /api/integrations/{id}/credentials."""

    def test_returns_decrypted_credentials(self, int_client):
        """Credentials endpoint returns the decrypted dict."""
        created = int_client.post("/api/integrations/", json=_IMAP_SOURCE).json()
        resp = int_client.get(f"/api/integrations/{created['id']}/credentials")
        assert resp.status_code == 200
        creds = resp.json()["credentials"]
        assert creds["password"] == "s3cr3t"  # noqa: S105

    def test_returns_empty_dict_when_no_credentials(self, int_client):
        """No credentials stored returns empty dict."""
        payload = dict(_IMAP_SOURCE, credentials=None)
        created = int_client.post("/api/integrations/", json=payload).json()
        resp = int_client.get(f"/api/integrations/{created['id']}/credentials")
        assert resp.status_code == 200
        assert resp.json()["credentials"] == {}

    def test_not_found(self, int_client):
        """Non-existent integration returns 404."""
        resp = int_client.get("/api/integrations/9999/credentials")
        assert resp.status_code == 404

    def test_other_users_credentials_returns_404(self, int_client, int_session):
        """Cannot retrieve another user's credentials."""
        other_integration = UserIntegration(
            owner_id=_OTHER_OWNER,
            direction="SOURCE",
            integration_type="IMAP",
            name="Other Mailbox",
            credentials='{"password": "secret"}',
            is_active=True,
        )
        int_session.add(other_integration)
        int_session.commit()
        resp = int_client.get(f"/api/integrations/{other_integration.id}/credentials")
        assert resp.status_code == 404


@pytest.mark.unit
class TestIntegrationModel:
    """Unit tests for the UserIntegration model and helper constants."""

    def test_integration_direction_constants(self):
        """IntegrationDirection has expected values."""
        from app.models import IntegrationDirection

        assert IntegrationDirection.SOURCE == "SOURCE"
        assert IntegrationDirection.DESTINATION == "DESTINATION"
        assert "SOURCE" in IntegrationDirection.ALL
        assert "DESTINATION" in IntegrationDirection.ALL

    def test_integration_type_constants(self):
        """IntegrationType has expected values."""
        from app.models import IntegrationType

        assert IntegrationType.IMAP == "IMAP"
        assert IntegrationType.S3 == "S3"
        assert IntegrationType.DROPBOX == "DROPBOX"
        assert IntegrationType.GOOGLE_DRIVE == "GOOGLE_DRIVE"
        assert IntegrationType.ONEDRIVE == "ONEDRIVE"
        assert IntegrationType.WEBDAV == "WEBDAV"
        assert IntegrationType.NEXTCLOUD == "NEXTCLOUD"
        assert IntegrationType.WATCH_FOLDER == "WATCH_FOLDER"
        assert IntegrationType.WEBHOOK == "WEBHOOK"
        assert IntegrationType.FTP == "FTP"
        assert IntegrationType.SFTP == "SFTP"
        assert IntegrationType.EMAIL == "EMAIL"
        assert IntegrationType.PAPERLESS == "PAPERLESS"
        assert IntegrationType.RCLONE == "RCLONE"

    def test_user_integration_tablename(self):
        """UserIntegration uses the correct table name."""
        assert UserIntegration.__tablename__ == "user_integrations"

    def test_user_integration_fields(self):
        """UserIntegration has all required columns."""
        cols = {c.key for c in UserIntegration.__table__.columns}
        expected = {
            "id",
            "owner_id",
            "direction",
            "integration_type",
            "name",
            "config",
            "credentials",
            "is_active",
            "last_used_at",
            "last_error",
            "created_at",
            "updated_at",
        }
        assert expected <= cols


@pytest.mark.unit
class TestCredentialHelpers:
    """Unit tests for the credential encode/decode helpers in integrations.py."""

    def test_encode_decode_round_trip(self):
        """Encoding then decoding returns the original dict."""
        from app.api.integrations import _decode_credentials, _encode_credentials

        original = {"password": "super_secret", "token": "abc123"}
        encoded = _encode_credentials(original)
        assert encoded is not None
        decoded = _decode_credentials(encoded)
        assert decoded == original

    def test_encode_none_returns_none(self):
        """Encoding None returns None."""
        from app.api.integrations import _encode_credentials

        assert _encode_credentials(None) is None

    def test_encode_empty_dict_returns_none(self):
        """Encoding an empty dict returns None."""
        from app.api.integrations import _encode_credentials

        assert _encode_credentials({}) is None

    def test_decode_none_returns_none(self):
        """Decoding None returns None."""
        from app.api.integrations import _decode_credentials

        assert _decode_credentials(None) is None

    def test_decode_empty_string_returns_none(self):
        """Decoding an empty string returns None."""
        from app.api.integrations import _decode_credentials

        assert _decode_credentials("") is None

    def test_decode_invalid_json_returns_none(self):
        """Decoding a non-JSON plaintext string returns None."""
        from app.api.integrations import _decode_credentials

        # A non-JSON plaintext string (no enc: prefix) that decrypt_value returns as-is
        assert _decode_credentials("not-valid-json") is None


@pytest.mark.unit
class TestImapPasswordEncryption:
    """Unit tests verifying that IMAP passwords are encrypted at rest."""

    def test_create_encrypts_password(self, int_engine):
        """Creating an IMAP account stores the password encrypted."""
        from app.api.imap_accounts import _get_owner_id
        from app.main import app
        from app.models import SubscriptionPlan, UserImapAccount, UserProfile

        Session = sessionmaker(bind=int_engine)

        def override_db():
            session = Session()
            try:
                yield session
            finally:
                session.close()

        def override_owner():
            return _OWNER

        # Seed a paid plan (max_mailboxes=0 means unlimited for paid) and profile
        setup_session = Session()
        plan = SubscriptionPlan(
            plan_id="paid",
            name="Paid",
            price_monthly=9.99,
            price_yearly=99.99,
            max_mailboxes=0,  # 0 = unlimited for paid plans
            is_active=True,
        )
        setup_session.add(plan)
        profile = UserProfile(user_id=_OWNER, subscription_tier="paid")
        setup_session.add(profile)
        setup_session.commit()
        setup_session.close()

        app.dependency_overrides[get_db] = override_db
        app.dependency_overrides[_get_owner_id] = override_owner
        try:
            with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client:
                payload = {
                    "name": "Test",
                    "host": "imap.example.com",
                    "port": 993,
                    "username": "user@example.com",
                    "password": "plaintext_password",
                    "use_ssl": True,
                    "delete_after_process": False,
                    "is_active": True,
                }
                resp = client.post("/api/imap-accounts/", json=payload)
                assert resp.status_code == 201

                # Verify the stored password is encrypted
                verify_session = Session()
                acct = verify_session.query(UserImapAccount).first()
                assert acct is not None
                assert acct.password != "plaintext_password"
                assert acct.password.startswith("enc:")
                verify_session.close()
        finally:
            app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# Quota enforcement tests
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestQuotaEnforcementDestinations:
    """Tests for destination quota enforcement on POST /api/integrations/."""

    def test_create_destination_blocked_at_limit(self, int_engine, int_session):
        """Users at the destination quota limit receive a 403."""
        _make_profile(int_session, tier="starter")
        _make_plan(int_session, tier="starter", max_storage_destinations=1, max_mailboxes=1)

        from app.api.integrations import _get_owner_id
        from app.main import app

        def override_db():
            Session = sessionmaker(bind=int_engine)
            session = Session()
            try:
                yield session
            finally:
                session.close()

        app.dependency_overrides[get_db] = override_db
        app.dependency_overrides[_get_owner_id] = lambda: _OWNER

        try:
            with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client:
                # First destination should succeed
                resp1 = client.post("/api/integrations/", json=_S3_DESTINATION)
                assert resp1.status_code == 201

                # Second destination should be blocked
                second = dict(_S3_DESTINATION, name="Second Bucket")
                resp2 = client.post("/api/integrations/", json=second)
                assert resp2.status_code == 403
                assert "limit" in resp2.json()["detail"].lower()
        finally:
            app.dependency_overrides.clear()

    def test_create_destination_allowed_under_limit(self, int_engine, int_session):
        """Users under the destination quota can create integrations."""
        _make_profile(int_session, tier="professional")
        _make_plan(int_session, tier="professional", max_storage_destinations=5, max_mailboxes=3)

        from app.api.integrations import _get_owner_id
        from app.main import app

        def override_db():
            Session = sessionmaker(bind=int_engine)
            session = Session()
            try:
                yield session
            finally:
                session.close()

        app.dependency_overrides[get_db] = override_db
        app.dependency_overrides[_get_owner_id] = lambda: _OWNER

        try:
            with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client:
                resp = client.post("/api/integrations/", json=_S3_DESTINATION)
                assert resp.status_code == 201
        finally:
            app.dependency_overrides.clear()

    def test_free_tier_allows_one_destination(self, int_engine, int_session):
        """Free tier allows exactly 1 destination."""
        _make_profile(int_session, tier="free")
        _make_plan(int_session, tier="free", max_storage_destinations=1, max_mailboxes=0)

        from app.api.integrations import _get_owner_id
        from app.main import app

        def override_db():
            Session = sessionmaker(bind=int_engine)
            session = Session()
            try:
                yield session
            finally:
                session.close()

        app.dependency_overrides[get_db] = override_db
        app.dependency_overrides[_get_owner_id] = lambda: _OWNER

        try:
            with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client:
                resp1 = client.post("/api/integrations/", json=_S3_DESTINATION)
                assert resp1.status_code == 201

                second = dict(_S3_DESTINATION, name="Second")
                resp2 = client.post("/api/integrations/", json=second)
                assert resp2.status_code == 403
        finally:
            app.dependency_overrides.clear()


@pytest.mark.integration
class TestQuotaEnforcementSources:
    """Tests for IMAP source quota enforcement on POST /api/integrations/."""

    def test_create_imap_source_blocked_on_free_tier(self, int_engine, int_session):
        """Free-tier users cannot add IMAP source integrations."""
        _make_profile(int_session, tier="free")
        _make_plan(int_session, tier="free", max_storage_destinations=1, max_mailboxes=0)

        from app.api.integrations import _get_owner_id
        from app.main import app

        def override_db():
            Session = sessionmaker(bind=int_engine)
            session = Session()
            try:
                yield session
            finally:
                session.close()

        app.dependency_overrides[get_db] = override_db
        app.dependency_overrides[_get_owner_id] = lambda: _OWNER

        try:
            with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client:
                resp = client.post("/api/integrations/", json=_IMAP_SOURCE)
                assert resp.status_code == 403
                assert "plan" in resp.json()["detail"].lower()
        finally:
            app.dependency_overrides.clear()

    def test_create_imap_source_blocked_at_limit(self, int_engine, int_session):
        """Starter-tier users with 1 IMAP source cannot add a second."""
        _make_profile(int_session, tier="starter")
        _make_plan(int_session, tier="starter", max_storage_destinations=2, max_mailboxes=1)

        from app.api.integrations import _get_owner_id
        from app.main import app

        def override_db():
            Session = sessionmaker(bind=int_engine)
            session = Session()
            try:
                yield session
            finally:
                session.close()

        app.dependency_overrides[get_db] = override_db
        app.dependency_overrides[_get_owner_id] = lambda: _OWNER

        try:
            with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client:
                resp1 = client.post("/api/integrations/", json=_IMAP_SOURCE)
                assert resp1.status_code == 201

                second = dict(_IMAP_SOURCE, name="Second Mailbox")
                resp2 = client.post("/api/integrations/", json=second)
                assert resp2.status_code == 403
        finally:
            app.dependency_overrides.clear()

    def test_create_imap_source_unlimited_on_power_tier(self, int_engine, int_session):
        """Power-tier users can add multiple IMAP sources (unlimited)."""
        _make_profile(int_session, tier="business")
        _make_plan(int_session, tier="business", max_storage_destinations=10, max_mailboxes=0)

        from app.api.integrations import _get_owner_id
        from app.main import app

        def override_db():
            Session = sessionmaker(bind=int_engine)
            session = Session()
            try:
                yield session
            finally:
                session.close()

        app.dependency_overrides[get_db] = override_db
        app.dependency_overrides[_get_owner_id] = lambda: _OWNER

        try:
            with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client:
                resp1 = client.post("/api/integrations/", json=_IMAP_SOURCE)
                resp2 = client.post("/api/integrations/", json=dict(_IMAP_SOURCE, name="Second"))
                resp3 = client.post("/api/integrations/", json=dict(_IMAP_SOURCE, name="Third"))
            assert resp1.status_code == 201
            assert resp2.status_code == 201
            assert resp3.status_code == 201
        finally:
            app.dependency_overrides.clear()

    def test_watch_folder_source_not_quota_limited(self, int_engine, int_session):
        """WATCH_FOLDER sources are not subject to mailbox quota limits."""
        _make_profile(int_session, tier="free")
        _make_plan(int_session, tier="free", max_storage_destinations=1, max_mailboxes=0)

        from app.api.integrations import _get_owner_id
        from app.main import app

        def override_db():
            Session = sessionmaker(bind=int_engine)
            session = Session()
            try:
                yield session
            finally:
                session.close()

        app.dependency_overrides[get_db] = override_db
        app.dependency_overrides[_get_owner_id] = lambda: _OWNER

        try:
            with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client:
                payload = {
                    "direction": "SOURCE",
                    "integration_type": "WATCH_FOLDER",
                    "name": "My Folder",
                    "config": {"path": "/tmp/watch"},
                    "is_active": True,
                }
                resp = client.post("/api/integrations/", json=payload)
                assert resp.status_code == 201
        finally:
            app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# Quota helpers unit tests
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestQuotaHelpers:
    """Unit tests for the quota helper functions."""

    def test_get_max_destinations_free_tier(self):
        from app.api.integrations import _get_max_destinations

        assert _get_max_destinations({"id": "free", "max_storage_destinations": 1}) == 1

    def test_get_max_destinations_paid_explicit(self):
        from app.api.integrations import _get_max_destinations

        assert _get_max_destinations({"id": "starter", "max_storage_destinations": 2}) == 2

    def test_get_max_destinations_paid_unlimited(self):
        from app.api.integrations import _get_max_destinations

        assert _get_max_destinations({"id": "business", "max_storage_destinations": 0}) is None

    def test_get_max_sources_free_tier(self):
        from app.api.integrations import _get_max_sources

        assert _get_max_sources({"id": "free", "max_mailboxes": 0}) == 0

    def test_get_max_sources_paid_explicit(self):
        from app.api.integrations import _get_max_sources

        assert _get_max_sources({"id": "starter", "max_mailboxes": 1}) == 1

    def test_get_max_sources_paid_unlimited(self):
        from app.api.integrations import _get_max_sources

        assert _get_max_sources({"id": "business", "max_mailboxes": 0}) is None


# ---------------------------------------------------------------------------
# Connection test endpoint tests
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestConnectionTestEndpoint:
    """Tests for POST /api/integrations/test."""

    def test_test_unsupported_type(self, int_client):
        """Unsupported integration types return a helpful non-error message."""
        payload = {
            "integration_type": "FTP",
            "config": {},
            "credentials": {"username": "user", "password": "pass"},
        }
        resp = int_client.post("/api/integrations/test", json=payload)
        assert resp.status_code == 200
        data = resp.json()
        assert data["success"] is False
        assert "not yet supported" in data["message"]

    def test_test_dropbox_missing_refresh_token(self, int_client):
        """Dropbox test with missing refresh_token returns failure."""
        payload = {
            "integration_type": "DROPBOX",
            "config": {},
            "credentials": {"app_key": "key", "app_secret": "secret"},
        }
        resp = int_client.post("/api/integrations/test", json=payload)
        assert resp.status_code == 200
        data = resp.json()
        assert data["success"] is False
        assert "refresh_token" in data["message"].lower()

    def test_test_dropbox_missing_app_key(self, int_client):
        """Dropbox test with missing app_key/app_secret returns failure."""
        payload = {
            "integration_type": "DROPBOX",
            "config": {},
            "credentials": {"refresh_token": "rtoken"},
        }
        resp = int_client.post("/api/integrations/test", json=payload)
        assert resp.status_code == 200
        data = resp.json()
        assert data["success"] is False
        assert "app_key" in data["message"].lower()

    def test_test_dropbox_invalid_credentials(self, int_client):
        """Dropbox test with bad credentials returns an auth failure."""
        from unittest.mock import MagicMock, patch

        import dropbox.exceptions as dbx_exc

        with patch("app.api.integrations.dbx_lib") as mock_dbx:
            mock_instance = MagicMock()
            mock_dbx.Dropbox.return_value = mock_instance
            mock_instance.users_get_current_account.side_effect = dbx_exc.AuthError("req_id", MagicMock())
            payload = {
                "integration_type": "DROPBOX",
                "config": {},
                "credentials": {
                    "app_key": "bad_key",
                    "app_secret": "bad_secret",
                    "refresh_token": "bad_token",
                },
            }
            resp = int_client.post("/api/integrations/test", json=payload)
            assert resp.status_code == 200
            data = resp.json()
            assert data["success"] is False
            assert "authentication failed" in data["message"].lower()

    def test_test_dropbox_success(self, int_client):
        """Dropbox test with valid (mocked) credentials returns success."""
        from unittest.mock import MagicMock, patch

        with patch("app.api.integrations.dbx_lib") as mock_dbx:
            mock_instance = MagicMock()
            mock_dbx.Dropbox.return_value = mock_instance
            mock_account = MagicMock()
            mock_account.name.display_name = "Test User"
            mock_instance.users_get_current_account.return_value = mock_account

            payload = {
                "integration_type": "DROPBOX",
                "config": {},
                "credentials": {
                    "app_key": "valid_key",
                    "app_secret": "valid_secret",
                    "refresh_token": "valid_token",
                },
            }
            resp = int_client.post("/api/integrations/test", json=payload)
            assert resp.status_code == 200
            data = resp.json()
            assert data["success"] is True
            assert "dropbox connection successful" in data["message"].lower()

    def test_test_invalid_type_returns_400(self, int_client):
        """Invalid integration_type returns 400."""
        payload = {
            "integration_type": "INVALID",
            "config": {},
        }
        resp = int_client.post("/api/integrations/test", json=payload)
        assert resp.status_code == 400

    def test_test_imap_missing_fields(self, int_client):
        """IMAP test with missing fields returns failure."""
        payload = {
            "integration_type": "IMAP",
            "config": {"host": ""},
            "credentials": {},
        }
        resp = int_client.post("/api/integrations/test", json=payload)
        assert resp.status_code == 200
        data = resp.json()
        assert data["success"] is False
        assert "Missing" in data["message"]

    def test_test_s3_missing_bucket(self, int_client):
        """S3 test with missing bucket returns failure."""
        payload = {
            "integration_type": "S3",
            "config": {},
            "credentials": {"access_key_id": "AKIA", "secret_access_key": "secret"},
        }
        resp = int_client.post("/api/integrations/test", json=payload)
        assert resp.status_code == 200
        data = resp.json()
        assert data["success"] is False
        assert "bucket" in data["message"].lower()

    def test_test_webdav_missing_url(self, int_client):
        """WebDAV test with missing URL returns failure."""
        payload = {
            "integration_type": "WEBDAV",
            "config": {},
            "credentials": {"username": "u", "password": "p"},
        }
        resp = int_client.post("/api/integrations/test", json=payload)
        assert resp.status_code == 200
        data = resp.json()
        assert data["success"] is False
        assert "url" in data["message"].lower()

    def test_test_webdav_blocks_private_ip(self, int_client):
        """WebDAV test blocks requests to private/internal IPs (SSRF protection)."""
        payload = {
            "integration_type": "WEBDAV",
            "config": {"url": "http://127.0.0.1/webdav"},
            "credentials": {"username": "u", "password": "p"},
        }
        resp = int_client.post("/api/integrations/test", json=payload)
        assert resp.status_code == 200
        data = resp.json()
        assert data["success"] is False
        assert "internal" in data["message"].lower() or "private" in data["message"].lower()

    def test_test_webdav_blocks_localhost(self, int_client):
        """WebDAV test blocks requests to localhost."""
        payload = {
            "integration_type": "WEBDAV",
            "config": {"url": "http://localhost/webdav"},
            "credentials": {},
        }
        resp = int_client.post("/api/integrations/test", json=payload)
        assert resp.status_code == 200
        data = resp.json()
        assert data["success"] is False
        assert "internal" in data["message"].lower() or "private" in data["message"].lower()

    def test_test_webdav_blocks_file_scheme(self, int_client):
        """WebDAV test blocks file:// scheme."""
        payload = {
            "integration_type": "WEBDAV",
            "config": {"url": "file:///etc/passwd"},
            "credentials": {},
        }
        resp = int_client.post("/api/integrations/test", json=payload)
        assert resp.status_code == 200
        data = resp.json()
        assert data["success"] is False
        assert "scheme" in data["message"].lower()


# ---------------------------------------------------------------------------
# Quota endpoint tests
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestQuotaEndpoint:
    """Tests for GET /api/integrations/quota/."""

    def test_quota_returns_tier_info(self, int_client):
        """Quota endpoint returns tier information and counts."""
        resp = int_client.get("/api/integrations/quota/")
        assert resp.status_code == 200
        data = resp.json()
        assert "tier_id" in data
        assert "tier_name" in data
        assert "destinations" in data
        assert "sources" in data
        assert "current_count" in data["destinations"]
        assert "max_allowed" in data["destinations"]
        assert "can_add" in data["destinations"]

    def test_quota_reflects_created_integrations(self, int_client):
        """Quota counts update after creating integrations."""
        int_client.post("/api/integrations/", json=_S3_DESTINATION)
        resp = int_client.get("/api/integrations/quota/")
        data = resp.json()
        assert data["destinations"]["current_count"] == 1

    def test_quota_free_tier(self, int_engine, int_session):
        """Free tier shows correct quota limits."""
        _make_profile(int_session, tier="free")
        _make_plan(int_session, tier="free", max_storage_destinations=1, max_mailboxes=0)

        from app.api.integrations import _get_owner_id
        from app.main import app

        def override_db():
            Session = sessionmaker(bind=int_engine)
            session = Session()
            try:
                yield session
            finally:
                session.close()

        app.dependency_overrides[get_db] = override_db
        app.dependency_overrides[_get_owner_id] = lambda: _OWNER

        try:
            with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client:
                resp = client.get("/api/integrations/quota/")
                data = resp.json()
                assert data["tier_id"] == "free"
                assert data["destinations"]["max_allowed"] == 1
                assert data["destinations"]["can_add"] is True
                assert data["sources"]["max_allowed"] == 0
                assert data["sources"]["can_add"] is False
        finally:
            app.dependency_overrides.clear()
