"""Tests for the per-user IMAP accounts API (app/api/imap_accounts.py)."""

from unittest.mock import MagicMock, patch

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, UserImapAccount, UserProfile

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

_OWNER = "test_user@example.com"
_BASIC_ACCOUNT = {
    "name": "Test Mailbox",
    "host": "imap.example.com",
    "port": 993,
    "username": "user@example.com",
    "password": "s3cr3t",  # noqa: S105
    "use_ssl": True,
    "delete_after_process": False,
    "is_active": True,
}


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


@pytest.fixture()
def imap_engine():
    """In-memory SQLite engine for IMAP account 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 imap_session(imap_engine):
    """DB session scoped to one test."""
    Session = sessionmaker(bind=imap_engine)
    session = Session()
    yield session
    session.close()


def _make_client(imap_engine, owner_id: str = _OWNER):
    """Return a TestClient that injects *owner_id* as the authenticated user."""
    from app.api.imap_accounts import _get_owner_id
    from app.main import app

    def override_db():
        Session = sessionmaker(bind=imap_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
    return app, override_db, override_owner


def _make_anon_client(imap_engine):
    """Return a TestClient without an authenticated user (401 expected)."""
    from app.main import app

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

    app.dependency_overrides[get_db] = override_db
    return app


@pytest.fixture()
def imap_client(imap_engine):
    """TestClient authenticated as _OWNER."""
    from app.api.imap_accounts import _get_owner_id
    from app.main import app

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

    def override_owner():
        return _OWNER

    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 c:
        yield c
    app.dependency_overrides.clear()


def _make_profile(session, owner: str = _OWNER, tier: str = "starter") -> UserProfile:
    """Create a UserProfile."""
    profile = UserProfile(user_id=owner, subscription_tier=tier)
    session.add(profile)
    session.commit()
    session.refresh(profile)
    return profile


def _make_plan(session, tier: str = "starter", max_mailboxes: int = 1) -> SubscriptionPlan:
    """Create a SubscriptionPlan row."""
    plan = SubscriptionPlan(
        plan_id=tier,
        name=tier.title(),
        price_monthly=2.99,
        price_yearly=28.99,
        max_mailboxes=max_mailboxes,
        is_active=True,
    )
    session.add(plan)
    session.commit()
    session.refresh(plan)
    return plan


# ---------------------------------------------------------------------------
# Unit tests – quota helper
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestGetMaxMailboxes:
    """Unit tests for the _get_max_mailboxes helper."""

    def test_free_tier_returns_zero(self):
        from app.api.imap_accounts import _get_max_mailboxes

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

    def test_paid_tier_with_explicit_limit(self):
        from app.api.imap_accounts import _get_max_mailboxes

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

    def test_paid_tier_unlimited(self):
        from app.api.imap_accounts import _get_max_mailboxes

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

    def test_professional_three(self):
        from app.api.imap_accounts import _get_max_mailboxes

        assert _get_max_mailboxes({"id": "professional", "max_mailboxes": 3}) == 3


# ---------------------------------------------------------------------------
# Integration tests – list endpoint
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestListImapAccounts:
    """Tests for GET /api/imap-accounts/."""

    def test_list_empty(self, imap_client, imap_session):
        """Listing accounts for a user with none returns empty list."""
        _make_profile(imap_session, tier="starter")
        _make_plan(imap_session, tier="starter", max_mailboxes=1)

        resp = imap_client.get("/api/imap-accounts/")
        assert resp.status_code == 200
        assert resp.json() == []

    def test_list_returns_own_accounts_only(self, imap_client, imap_session):
        """Only the current user's accounts are returned."""
        _make_profile(imap_session, tier="starter")
        _make_plan(imap_session, tier="starter", max_mailboxes=2)

        acct = UserImapAccount(owner_id=_OWNER, name="Mine", host="h", port=993, username="u", password="p")
        other = UserImapAccount(
            owner_id="other@example.com",
            name="Theirs",
            host="h2",
            port=993,
            username="u",
            password="p",
        )
        imap_session.add_all([acct, other])
        imap_session.commit()

        resp = imap_client.get("/api/imap-accounts/")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data) == 1
        assert data[0]["name"] == "Mine"

    def test_list_requires_authentication(self, imap_engine):
        """Unauthenticated requests return 401."""
        from fastapi import HTTPException, status

        from app.api.imap_accounts import _get_owner_id
        from app.main import app

        def raise_401():
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")

        def override_db():
            from sqlalchemy.orm import sessionmaker

            Session = sessionmaker(bind=imap_engine)
            session = Session()
            try:
                yield session
            finally:
                session.close()

        app.dependency_overrides[get_db] = override_db
        app.dependency_overrides[_get_owner_id] = raise_401
        try:
            with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as c:
                resp = c.get("/api/imap-accounts/")
            assert resp.status_code == 401
        finally:
            app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# Integration tests – create endpoint
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestCreateImapAccount:
    """Tests for POST /api/imap-accounts/."""

    def test_create_success(self, imap_client, imap_session):
        """A valid create request returns 201 and the new account."""
        _make_profile(imap_session, tier="starter")
        _make_plan(imap_session, tier="starter", max_mailboxes=2)

        resp = imap_client.post("/api/imap-accounts/", json=_BASIC_ACCOUNT)
        assert resp.status_code == 201
        data = resp.json()
        assert data["name"] == _BASIC_ACCOUNT["name"]
        assert data["host"] == _BASIC_ACCOUNT["host"]
        assert "password" not in data

    def test_create_increments_count(self, imap_client, imap_session):
        """After creation, the count in the database increases."""
        _make_profile(imap_session, tier="starter")
        _make_plan(imap_session, tier="starter", max_mailboxes=2)

        imap_client.post("/api/imap-accounts/", json=_BASIC_ACCOUNT)
        count = imap_session.query(UserImapAccount).filter(UserImapAccount.owner_id == _OWNER).count()
        assert count == 1

    def test_create_blocked_on_free_tier(self, imap_client, imap_session):
        """Free-tier users cannot add any IMAP accounts."""
        _make_profile(imap_session, tier="free")
        _make_plan(imap_session, tier="free", max_mailboxes=0)

        resp = imap_client.post("/api/imap-accounts/", json=_BASIC_ACCOUNT)
        assert resp.status_code == 403
        assert "plan" in resp.json()["detail"].lower()

    def test_create_blocked_at_quota_limit(self, imap_client, imap_session):
        """Users at the quota limit receive a 403."""
        _make_profile(imap_session, tier="starter")
        _make_plan(imap_session, tier="starter", max_mailboxes=1)

        existing = UserImapAccount(owner_id=_OWNER, name="Existing", host="h", port=993, username="u", password="p")
        imap_session.add(existing)
        imap_session.commit()

        resp = imap_client.post("/api/imap-accounts/", json=_BASIC_ACCOUNT)
        assert resp.status_code == 403

    def test_create_unlimited_on_power_tier(self, imap_engine, imap_session):
        """Power-tier users can add multiple accounts (unlimited)."""
        from app.api.imap_accounts import _get_owner_id
        from app.main import app

        def override_db():
            from sqlalchemy.orm import sessionmaker

            Session = sessionmaker(bind=imap_engine)
            session = Session()
            try:
                yield session
            finally:
                session.close()

        def override_owner():
            return _OWNER

        _make_profile(imap_session, tier="business")
        _make_plan(imap_session, tier="business", max_mailboxes=0)

        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 c:
                resp1 = c.post("/api/imap-accounts/", json=_BASIC_ACCOUNT)
                resp2 = c.post("/api/imap-accounts/", json={**_BASIC_ACCOUNT, "name": "Second"})
            assert resp1.status_code == 201
            assert resp2.status_code == 201
        finally:
            app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# Integration tests – update endpoint
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestUpdateImapAccount:
    """Tests for PUT /api/imap-accounts/{id}."""

    def _create_account(self, session, owner: str = _OWNER) -> UserImapAccount:
        acct = UserImapAccount(owner_id=owner, name="Original", host="h", port=993, username="u", password="p")
        session.add(acct)
        session.commit()
        session.refresh(acct)
        return acct

    def test_update_name(self, imap_client, imap_session):
        """Updating name only changes the name."""
        _make_profile(imap_session, tier="starter")
        _make_plan(imap_session, tier="starter", max_mailboxes=1)
        acct = self._create_account(imap_session)

        resp = imap_client.put(f"/api/imap-accounts/{acct.id}", json={"name": "Updated"})
        assert resp.status_code == 200
        assert resp.json()["name"] == "Updated"
        assert resp.json()["host"] == "h"

    def test_update_not_found(self, imap_client, imap_session):
        """Updating a non-existent account returns 404."""
        resp = imap_client.put("/api/imap-accounts/9999", json={"name": "x"})
        assert resp.status_code == 404

    def test_cannot_update_other_users_account(self, imap_client, imap_session):
        """A user cannot update another user's account."""
        other_acct = self._create_account(imap_session, owner="other@example.com")
        resp = imap_client.put(f"/api/imap-accounts/{other_acct.id}", json={"name": "Hijacked"})
        assert resp.status_code == 404


# ---------------------------------------------------------------------------
# Integration tests – delete endpoint
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestDeleteImapAccount:
    """Tests for DELETE /api/imap-accounts/{id}."""

    def _create_account(self, session) -> UserImapAccount:
        acct = UserImapAccount(owner_id=_OWNER, name="ToDelete", host="h", port=993, username="u", password="p")
        session.add(acct)
        session.commit()
        session.refresh(acct)
        return acct

    def test_delete_success(self, imap_client, imap_session):
        """Deleting an account returns 204 and removes the DB row."""
        acct = self._create_account(imap_session)

        resp = imap_client.delete(f"/api/imap-accounts/{acct.id}")
        assert resp.status_code == 204

        remaining = imap_session.query(UserImapAccount).filter(UserImapAccount.id == acct.id).first()
        assert remaining is None

    def test_delete_not_found(self, imap_client, imap_session):
        """Deleting a non-existent account returns 404."""
        resp = imap_client.delete("/api/imap-accounts/9999")
        assert resp.status_code == 404


# ---------------------------------------------------------------------------
# Integration tests – test-connection endpoints
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestImapConnectionTest:
    """Tests for POST /api/imap-accounts/test and /{id}/test."""

    def test_test_connection_success(self, imap_client, imap_session):
        """A successful IMAP login returns success=True."""
        with patch("app.api.imap_accounts._test_imap_connection") as mock_test:
            mock_test.return_value = {"success": True, "message": "Connection successful"}
            resp = imap_client.post(
                "/api/imap-accounts/test",
                json={
                    "host": "imap.example.com",
                    "port": 993,
                    "username": "u@example.com",
                    "password": "pass",  # noqa: S106
                    "use_ssl": True,
                },
            )
        assert resp.status_code == 200
        assert resp.json()["success"] is True

    def test_test_connection_failure(self, imap_client, imap_session):
        """A failed IMAP login returns success=False with a message."""
        with patch("app.api.imap_accounts._test_imap_connection") as mock_test:
            mock_test.return_value = {
                "success": False,
                "message": "IMAP error: authentication failed",
            }
            resp = imap_client.post(
                "/api/imap-accounts/test",
                json={
                    "host": "imap.example.com",
                    "port": 993,
                    "username": "u@example.com",
                    "password": "wrong",  # noqa: S106
                    "use_ssl": True,
                },
            )
        assert resp.status_code == 200
        assert resp.json()["success"] is False

    def test_test_saved_account(self, imap_client, imap_session):
        """Testing a saved account calls the connection helper with stored credentials."""
        acct = UserImapAccount(
            owner_id=_OWNER,
            name="Saved",
            host="imap.test.com",
            port=993,
            username="u",
            password="p",
        )
        imap_session.add(acct)
        imap_session.commit()
        imap_session.refresh(acct)

        with patch("app.api.imap_accounts._test_imap_connection") as mock_test:
            mock_test.return_value = {"success": True, "message": "Connection successful"}
            resp = imap_client.post(f"/api/imap-accounts/{acct.id}/test")

        assert resp.status_code == 200
        assert resp.json()["success"] is True
        mock_test.assert_called_once_with("imap.test.com", 993, "u", "p", True)


# ---------------------------------------------------------------------------
# Integration tests – quota endpoint
# ---------------------------------------------------------------------------


@pytest.mark.integration
class TestImapQuota:
    """Tests for GET /api/imap-accounts/quota/."""

    def test_quota_free_tier(self, imap_client, imap_session):
        """Free-tier user reports max_mailboxes=0 and can_add=False."""
        _make_profile(imap_session, tier="free")
        _make_plan(imap_session, tier="free", max_mailboxes=0)

        resp = imap_client.get("/api/imap-accounts/quota/")
        assert resp.status_code == 200
        data = resp.json()
        assert data["max_mailboxes"] == 0
        assert data["can_add"] is False

    def test_quota_starter_tier(self, imap_client, imap_session):
        """Starter-tier user with no accounts reports can_add=True."""
        _make_profile(imap_session, tier="starter")
        _make_plan(imap_session, tier="starter", max_mailboxes=1)

        resp = imap_client.get("/api/imap-accounts/quota/")
        assert resp.status_code == 200
        data = resp.json()
        assert data["max_mailboxes"] == 1
        assert data["can_add"] is True
        assert data["current_count"] == 0

    def test_quota_unlimited(self, imap_client, imap_session):
        """Power-tier user reports max_mailboxes=None and can_add=True."""
        _make_profile(imap_session, tier="business")
        _make_plan(imap_session, tier="business", max_mailboxes=0)

        resp = imap_client.get("/api/imap-accounts/quota/")
        assert resp.status_code == 200
        data = resp.json()
        assert data["max_mailboxes"] is None
        assert data["can_add"] is True


# ---------------------------------------------------------------------------
# Unit tests – _test_imap_connection helper
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestTestImapConnection:
    """Unit tests for the _test_imap_connection helper (no network calls)."""

    def test_success(self):
        """Successful login returns success=True."""
        from app.api.imap_accounts import _test_imap_connection

        mock_mail = MagicMock()
        with patch("imaplib.IMAP4_SSL", return_value=mock_mail):
            result = _test_imap_connection(
                "imap.example.com",
                993,
                "user",
                "pass",
                use_ssl=True,  # noqa: S106
            )

        assert result["success"] is True
        mock_mail.login.assert_called_once_with("user", "pass")
        mock_mail.logout.assert_called_once()

    def test_auth_error(self):
        """An exception raised by IMAP4_SSL returns success=False."""
        from app.api.imap_accounts import _test_imap_connection

        with patch("imaplib.IMAP4_SSL", side_effect=Exception("auth failed")):
            result = _test_imap_connection(
                "imap.example.com",
                993,
                "user",
                "badpass",
                use_ssl=True,  # noqa: S106
            )

        assert result["success"] is False
        assert "auth failed" in result["message"]

    def test_network_error(self):
        """An OSError returns success=False with a network error message."""
        from app.api.imap_accounts import _test_imap_connection

        with patch("imaplib.IMAP4", side_effect=OSError("connection refused")):
            result = _test_imap_connection(
                "bad-host",
                143,
                "user",
                "pass",
                use_ssl=False,  # noqa: S106
            )

        assert result["success"] is False
        assert "connection refused" in result["message"].lower()
