"""Tests for the per-user notification system (app/api/notifications.py)."""

import json

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 InAppNotification, UserNotificationPreference, UserNotificationTarget

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

_OWNER = "notifuser@example.com"
_OTHER_OWNER = "other@example.com"


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


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


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

    Session = sessionmaker(bind=notif_engine)

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

    def _override_owner():
        return owner_id

    app.dependency_overrides[get_db] = _override_get_db
    app.dependency_overrides[_get_owner_id] = _override_owner

    return TestClient(app, base_url="http://localhost", raise_server_exceptions=False)


def _cleanup(app):
    """Remove dependency overrides after test."""
    app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# Tests – Auth / 401 guard
# ---------------------------------------------------------------------------


class TestAuthGuard:
    """Verify that unauthenticated requests are rejected."""

    @pytest.mark.unit
    def test_inbox_requires_auth(self):
        """GET /api/user-notifications/inbox should return 401 when not authenticated."""
        from app.main import app

        client = TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
        resp = client.get("/api/user-notifications/inbox")
        assert resp.status_code == 401

    @pytest.mark.unit
    def test_unread_count_requires_auth(self):
        """GET /api/user-notifications/inbox/unread-count should return 401 when not authenticated."""
        from app.main import app

        client = TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
        resp = client.get("/api/user-notifications/inbox/unread-count")
        assert resp.status_code == 401


# ---------------------------------------------------------------------------
# Tests – Inbox
# ---------------------------------------------------------------------------


class TestInbox:
    """Tests for the in-app notification inbox."""

    @pytest.mark.unit
    def test_unread_count_empty(self, notif_engine):
        """Unread count should be 0 when no notifications exist."""
        from app.main import app

        client = _make_client(notif_engine)
        try:
            resp = client.get("/api/user-notifications/inbox/unread-count")
            assert resp.status_code == 200
            assert resp.json() == {"count": 0}
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_inbox_empty(self, notif_engine):
        """Listing inbox when empty should return an empty list."""
        from app.main import app

        client = _make_client(notif_engine)
        try:
            resp = client.get("/api/user-notifications/inbox")
            assert resp.status_code == 200
            assert resp.json() == []
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_inbox_shows_notifications(self, notif_engine, notif_session):
        """Inbox should return notifications for the authenticated user."""
        from app.main import app

        notif_session.add(
            InAppNotification(
                owner_id=_OWNER,
                event_type="document.processed",
                title="Test",
                message="Done",
            )
        )
        notif_session.commit()

        client = _make_client(notif_engine)
        try:
            resp = client.get("/api/user-notifications/inbox")
            assert resp.status_code == 200
            items = resp.json()
            assert len(items) == 1
            assert items[0]["title"] == "Test"
            assert items[0]["is_read"] is False
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_inbox_isolation(self, notif_engine, notif_session):
        """Users should only see their own notifications."""
        from app.main import app

        notif_session.add(
            InAppNotification(
                owner_id=_OTHER_OWNER,
                event_type="document.processed",
                title="Other user notif",
                message="Not yours",
            )
        )
        notif_session.commit()

        client = _make_client(notif_engine, _OWNER)
        try:
            resp = client.get("/api/user-notifications/inbox")
            assert resp.status_code == 200
            assert resp.json() == []
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_unread_count_reflects_notifications(self, notif_engine, notif_session):
        """Unread count should reflect actual unread notifications."""
        from app.main import app

        for i in range(3):
            notif_session.add(
                InAppNotification(
                    owner_id=_OWNER,
                    event_type="document.processed",
                    title=f"Notif {i}",
                    message="",
                    is_read=False,
                )
            )
        notif_session.commit()

        client = _make_client(notif_engine)
        try:
            resp = client.get("/api/user-notifications/inbox/unread-count")
            assert resp.status_code == 200
            assert resp.json()["count"] == 3
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_mark_read(self, notif_engine, notif_session):
        """Marking a notification as read should update is_read."""
        from app.main import app

        notif = InAppNotification(
            owner_id=_OWNER,
            event_type="document.processed",
            title="Unread",
            message="",
        )
        notif_session.add(notif)
        notif_session.commit()
        notif_session.refresh(notif)
        notif_id = notif.id

        client = _make_client(notif_engine)
        try:
            resp = client.post(f"/api/user-notifications/inbox/{notif_id}/read")
            assert resp.status_code == 200

            # Verify in DB
            notif_session.refresh(notif)
            assert notif.is_read is True
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_mark_read_wrong_user(self, notif_engine, notif_session):
        """Marking another user's notification should return 404."""
        from app.main import app

        notif = InAppNotification(
            owner_id=_OTHER_OWNER,
            event_type="document.processed",
            title="Other",
            message="",
        )
        notif_session.add(notif)
        notif_session.commit()
        notif_session.refresh(notif)
        notif_id = notif.id

        client = _make_client(notif_engine, _OWNER)
        try:
            resp = client.post(f"/api/user-notifications/inbox/{notif_id}/read")
            assert resp.status_code == 404
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_mark_all_read(self, notif_engine, notif_session):
        """Mark all read should set all user's notifications to read."""
        from app.main import app

        for i in range(4):
            notif_session.add(
                InAppNotification(
                    owner_id=_OWNER,
                    event_type="document.processed",
                    title=f"N{i}",
                    message="",
                    is_read=False,
                )
            )
        notif_session.commit()

        client = _make_client(notif_engine)
        try:
            resp = client.post("/api/user-notifications/inbox/read-all")
            assert resp.status_code == 200

            count_resp = client.get("/api/user-notifications/inbox/unread-count")
            assert count_resp.json()["count"] == 0
        finally:
            _cleanup(app)


# ---------------------------------------------------------------------------
# Tests – Notification Targets
# ---------------------------------------------------------------------------


class TestTargets:
    """Tests for notification target CRUD."""

    @pytest.mark.unit
    def test_list_targets_empty(self, notif_engine):
        """Listing targets when none exist should return empty list."""
        from app.main import app

        client = _make_client(notif_engine)
        try:
            resp = client.get("/api/user-notifications/targets")
            assert resp.status_code == 200
            assert resp.json() == []
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_create_email_target(self, notif_engine):
        """Creating an email target should persist and mask the password in response."""
        from app.main import app

        client = _make_client(notif_engine)
        try:
            resp = client.post(
                "/api/user-notifications/targets",
                json={
                    "channel_type": "email",
                    "name": "My Gmail",
                    "config": {
                        "smtp_host": "smtp.gmail.com",
                        "smtp_port": 587,
                        "smtp_username": "me@gmail.com",
                        "smtp_password": "s3cr3t",
                        "recipient_email": "me@gmail.com",
                        "smtp_use_tls": True,
                    },
                    "is_active": True,
                },
            )
            assert resp.status_code == 201, resp.text
            data = resp.json()
            assert data["channel_type"] == "email"
            assert data["name"] == "My Gmail"
            assert data["is_active"] is True
            # Password must be masked
            assert data["config"]["smtp_password"] == "****"
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_create_webhook_target(self, notif_engine):
        """Creating a webhook target should persist correctly."""
        from app.main import app

        client = _make_client(notif_engine)
        try:
            resp = client.post(
                "/api/user-notifications/targets",
                json={
                    "channel_type": "webhook",
                    "name": "Slack Webhook",
                    "config": {"url": "https://hooks.slack.com/abc", "secret": ""},
                    "is_active": True,
                },
            )
            assert resp.status_code == 201, resp.text
            data = resp.json()
            assert data["channel_type"] == "webhook"
            assert data["name"] == "Slack Webhook"
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_create_target_invalid_channel_type(self, notif_engine):
        """Creating a target with an invalid channel_type should return 422."""
        from app.main import app

        client = _make_client(notif_engine)
        try:
            resp = client.post(
                "/api/user-notifications/targets",
                json={"channel_type": "sms", "name": "Bad", "config": {}},
            )
            assert resp.status_code == 422
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_list_targets_returns_created(self, notif_engine):
        """Listing targets should include newly created ones."""
        from app.main import app

        client = _make_client(notif_engine)
        try:
            client.post(
                "/api/user-notifications/targets",
                json={"channel_type": "webhook", "name": "W1", "config": {"url": "https://example.com"}},
            )
            client.post(
                "/api/user-notifications/targets",
                json={"channel_type": "email", "name": "E1", "config": {"smtp_host": "smtp.example.com"}},
            )
            resp = client.get("/api/user-notifications/targets")
            assert resp.status_code == 200
            assert len(resp.json()) == 2
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_targets_isolation(self, notif_engine):
        """Users should only see their own targets."""
        from app.main import app

        client_a = _make_client(notif_engine, _OWNER)
        try:
            client_a.post(
                "/api/user-notifications/targets",
                json={"channel_type": "webhook", "name": "Owner A target", "config": {"url": "https://a.example.com"}},
            )
        finally:
            _cleanup(app)

        client_b = _make_client(notif_engine, _OTHER_OWNER)
        try:
            resp = client_b.get("/api/user-notifications/targets")
            assert resp.status_code == 200
            assert resp.json() == []
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_update_target(self, notif_engine):
        """Updating a target should change its name and active status."""
        from app.main import app

        client = _make_client(notif_engine)
        try:
            create_resp = client.post(
                "/api/user-notifications/targets",
                json={"channel_type": "webhook", "name": "Old Name", "config": {"url": "https://x.com"}},
            )
            target_id = create_resp.json()["id"]

            resp = client.put(
                f"/api/user-notifications/targets/{target_id}",
                json={"name": "New Name", "is_active": False},
            )
            assert resp.status_code == 200
            data = resp.json()
            assert data["name"] == "New Name"
            assert data["is_active"] is False
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_update_target_wrong_user(self, notif_engine, notif_session):
        """Updating another user's target should return 404."""
        from app.main import app

        target = UserNotificationTarget(
            owner_id=_OTHER_OWNER,
            channel_type="webhook",
            name="Other target",
            config=json.dumps({"url": "https://other.com"}),
        )
        notif_session.add(target)
        notif_session.commit()
        notif_session.refresh(target)

        client = _make_client(notif_engine, _OWNER)
        try:
            resp = client.put(
                f"/api/user-notifications/targets/{target.id}",
                json={"name": "Hacked"},
            )
            assert resp.status_code == 404
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_delete_target(self, notif_engine):
        """Deleting a target should remove it from the list."""
        from app.main import app

        client = _make_client(notif_engine)
        try:
            create_resp = client.post(
                "/api/user-notifications/targets",
                json={"channel_type": "webhook", "name": "To Delete", "config": {"url": "https://x.com"}},
            )
            target_id = create_resp.json()["id"]

            del_resp = client.delete(f"/api/user-notifications/targets/{target_id}")
            assert del_resp.status_code == 200

            list_resp = client.get("/api/user-notifications/targets")
            assert list_resp.json() == []
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_delete_target_wrong_user(self, notif_engine, notif_session):
        """Deleting another user's target should return 404."""
        from app.main import app

        target = UserNotificationTarget(
            owner_id=_OTHER_OWNER,
            channel_type="webhook",
            name="Not yours",
            config=json.dumps({"url": "https://other.com"}),
        )
        notif_session.add(target)
        notif_session.commit()
        notif_session.refresh(target)

        client = _make_client(notif_engine, _OWNER)
        try:
            resp = client.delete(f"/api/user-notifications/targets/{target.id}")
            assert resp.status_code == 404
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_delete_target_also_removes_preferences(self, notif_engine, notif_session):
        """Deleting a target should also remove associated preferences."""
        from app.main import app

        target = UserNotificationTarget(
            owner_id=_OWNER,
            channel_type="webhook",
            name="With prefs",
            config=json.dumps({"url": "https://x.com"}),
        )
        notif_session.add(target)
        notif_session.commit()
        notif_session.refresh(target)

        pref = UserNotificationPreference(
            owner_id=_OWNER,
            event_type="document.processed",
            channel_type="webhook",
            target_id=target.id,
            is_enabled=True,
        )
        notif_session.add(pref)
        notif_session.commit()

        client = _make_client(notif_engine, _OWNER)
        try:
            resp = client.delete(f"/api/user-notifications/targets/{target.id}")
            assert resp.status_code == 200

            remaining = (
                notif_session.query(UserNotificationPreference)
                .filter(UserNotificationPreference.owner_id == _OWNER)
                .all()
            )
            assert remaining == []
        finally:
            _cleanup(app)


# ---------------------------------------------------------------------------
# Tests – Preferences
# ---------------------------------------------------------------------------


class TestPreferences:
    """Tests for notification preferences CRUD."""

    @pytest.mark.unit
    def test_get_preferences_empty(self, notif_engine):
        """Getting preferences returns event_types and event_labels even with no prefs set."""
        from app.main import app

        client = _make_client(notif_engine)
        try:
            resp = client.get("/api/user-notifications/preferences")
            assert resp.status_code == 200
            data = resp.json()
            assert "event_types" in data
            assert "event_labels" in data
            assert "preferences" in data
            assert "document.processed" in data["event_types"]
            assert "document.failed" in data["event_types"]
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_update_preferences(self, notif_engine, notif_session):
        """Updating preferences should persist the changes."""
        from app.main import app

        target = UserNotificationTarget(
            owner_id=_OWNER,
            channel_type="webhook",
            name="My Webhook",
            config=json.dumps({"url": "https://x.com"}),
        )
        notif_session.add(target)
        notif_session.commit()
        notif_session.refresh(target)

        client = _make_client(notif_engine, _OWNER)
        try:
            resp = client.put(
                "/api/user-notifications/preferences",
                json={
                    "preferences": [
                        {
                            "event_type": "document.processed",
                            "channel_type": "webhook",
                            "is_enabled": True,
                            "target_id": target.id,
                        }
                    ]
                },
            )
            assert resp.status_code == 200

            # Verify stored
            pref = (
                notif_session.query(UserNotificationPreference)
                .filter(
                    UserNotificationPreference.owner_id == _OWNER,
                    UserNotificationPreference.event_type == "document.processed",
                    UserNotificationPreference.channel_type == "webhook",
                )
                .first()
            )
            assert pref is not None
            assert pref.is_enabled is True
            assert pref.target_id == target.id
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_update_preferences_upsert(self, notif_engine, notif_session):
        """Updating preferences twice should upsert (not duplicate)."""
        from app.main import app

        target = UserNotificationTarget(
            owner_id=_OWNER,
            channel_type="webhook",
            name="W",
            config=json.dumps({"url": "https://x.com"}),
        )
        notif_session.add(target)
        notif_session.commit()
        notif_session.refresh(target)

        client = _make_client(notif_engine, _OWNER)
        try:
            pref_item = {
                "event_type": "document.processed",
                "channel_type": "webhook",
                "is_enabled": True,
                "target_id": target.id,
            }
            client.put("/api/user-notifications/preferences", json={"preferences": [pref_item]})
            # Disable it
            pref_item["is_enabled"] = False
            resp = client.put("/api/user-notifications/preferences", json={"preferences": [pref_item]})
            assert resp.status_code == 200

            prefs = (
                notif_session.query(UserNotificationPreference)
                .filter(UserNotificationPreference.owner_id == _OWNER)
                .all()
            )
            assert len(prefs) == 1
            assert prefs[0].is_enabled is False
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_update_preferences_rejects_foreign_target(self, notif_engine, notif_session):
        """Preferences referencing another user's target_id should be rejected."""
        from app.main import app

        other_target = UserNotificationTarget(
            owner_id=_OTHER_OWNER,
            channel_type="webhook",
            name="Other webhook",
            config=json.dumps({"url": "https://other.com"}),
        )
        notif_session.add(other_target)
        notif_session.commit()
        notif_session.refresh(other_target)

        client = _make_client(notif_engine, _OWNER)
        try:
            resp = client.put(
                "/api/user-notifications/preferences",
                json={
                    "preferences": [
                        {
                            "event_type": "document.processed",
                            "channel_type": "webhook",
                            "is_enabled": True,
                            "target_id": other_target.id,
                        }
                    ]
                },
            )
            assert resp.status_code == 400
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_get_preferences_reflects_saved(self, notif_engine, notif_session):
        """GET preferences should reflect previously saved preferences."""
        from app.main import app

        target = UserNotificationTarget(
            owner_id=_OWNER,
            channel_type="email",
            name="Email target",
            config=json.dumps({"smtp_host": "smtp.example.com", "recipient_email": "me@example.com"}),
        )
        notif_session.add(target)
        notif_session.commit()
        notif_session.refresh(target)

        notif_session.add(
            UserNotificationPreference(
                owner_id=_OWNER,
                event_type="document.failed",
                channel_type="email",
                target_id=target.id,
                is_enabled=True,
            )
        )
        notif_session.commit()

        client = _make_client(notif_engine, _OWNER)
        try:
            resp = client.get("/api/user-notifications/preferences")
            assert resp.status_code == 200
            data = resp.json()
            assert "document.failed" in data["preferences"]
            assert "email" in data["preferences"]["document.failed"]
            assert data["preferences"]["document.failed"]["email"]["is_enabled"] is True
        finally:
            _cleanup(app)


# ---------------------------------------------------------------------------
# Tests – user_notification service
# ---------------------------------------------------------------------------


class TestUserNotificationService:
    """Unit tests for the user notification dispatch service."""

    @pytest.mark.unit
    def test_create_in_app_notification(self, notif_engine, notif_session):
        """create_in_app_notification should persist a record."""
        from unittest.mock import patch

        from app.utils.user_notification import create_in_app_notification

        Session = sessionmaker(bind=notif_engine)

        with patch("app.utils.user_notification.SessionLocal", Session):
            result = create_in_app_notification(
                owner_id=_OWNER,
                event_type="document.processed",
                title="Test",
                message="Done",
                file_id=42,
            )

        assert result is not None
        assert result.owner_id == _OWNER
        assert result.title == "Test"
        assert result.file_id == 42

    @pytest.mark.unit
    def test_notify_user_document_processed(self, notif_engine):
        """notify_user_document_processed should create an in-app notification."""
        from unittest.mock import patch

        from app.utils.user_notification import notify_user_document_processed

        Session = sessionmaker(bind=notif_engine)

        with patch("app.utils.user_notification.SessionLocal", Session):
            notify_user_document_processed(owner_id=_OWNER, filename="test.pdf", file_id=1)

        s = Session()
        notifs = s.query(InAppNotification).filter(InAppNotification.owner_id == _OWNER).all()
        s.close()
        assert len(notifs) == 1
        assert "test.pdf" in notifs[0].title

    @pytest.mark.unit
    def test_notify_user_document_failed(self, notif_engine):
        """notify_user_document_failed should create an in-app notification."""
        from unittest.mock import patch

        from app.utils.user_notification import notify_user_document_failed

        Session = sessionmaker(bind=notif_engine)

        with patch("app.utils.user_notification.SessionLocal", Session):
            notify_user_document_failed(owner_id=_OWNER, filename="doc.pdf", error="OCR timeout")

        s = Session()
        notifs = s.query(InAppNotification).filter(InAppNotification.owner_id == _OWNER).all()
        s.close()
        assert len(notifs) == 1
        assert notifs[0].event_type == "document.failed"
        assert "OCR timeout" in notifs[0].message

    @pytest.mark.unit
    def test_send_webhook_notification_missing_url(self):
        """_send_webhook_notification should return False when url is missing."""
        from app.utils.user_notification import _send_webhook_notification

        result = _send_webhook_notification({}, "document.processed", "Title", "Body")
        assert result is False

    @pytest.mark.unit
    def test_send_email_notification_missing_host(self):
        """_send_email_notification should return False when smtp_host is missing."""
        from app.utils.user_notification import _send_email_notification

        result = _send_email_notification({"recipient_email": "me@example.com"}, "Title", "Body")
        assert result is False

    @pytest.mark.unit
    def test_send_email_notification_missing_recipient(self):
        """_send_email_notification should return False when recipient_email is missing."""
        from app.utils.user_notification import _send_email_notification

        result = _send_email_notification({"smtp_host": "smtp.example.com"}, "Title", "Body")
        assert result is False


class TestBenchmark:
    @pytest.mark.unit
    def test_update_preferences_benchmark(self, notif_engine, notif_session):
        import statistics
        import time

        from app.main import app

        target = UserNotificationTarget(
            owner_id=_OWNER,
            channel_type="webhook",
            name="My Webhook",
            config=json.dumps({"url": "https://x.com"}),
        )
        notif_session.add(target)
        notif_session.commit()
        notif_session.refresh(target)

        client = _make_client(notif_engine, _OWNER)
        try:
            items_count = 100
            preferences = []
            for i in range(items_count):
                preferences.append(
                    {
                        "event_type": f"event.type.{i}",
                        "channel_type": "webhook",
                        "is_enabled": True,
                        "target_id": target.id,
                    }
                )

            payload = {"preferences": preferences}

            # Warm up
            client.put("/api/user-notifications/preferences", json=payload)

            times = []
            for _ in range(5):
                # Alter the values a bit so it's a real update
                for p in payload["preferences"]:
                    p["is_enabled"] = not p["is_enabled"]

                start = time.time()
                resp = client.put("/api/user-notifications/preferences", json=payload)
                end = time.time()

                assert resp.status_code == 200
                times.append(end - start)

            print(f"\nAverage time: {statistics.mean(times):.4f}s")
        finally:
            _cleanup(app)
