"""Tests for the mobile API endpoints (app/api/mobile.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 ApiToken, MobileDevice

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

_OWNER = "mobile_user@example.com"
_OTHER_OWNER = "other@example.com"
_EXPO_TOKEN = "ExponentPushToken[test-token-abc123]"

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


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


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

    Session = sessionmaker(bind=mob_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

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


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


# ---------------------------------------------------------------------------
# Tests – /mobile/generate-token
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestGenerateMobileToken:
    """Tests for POST /api/mobile/generate-token."""

    def test_generate_token_success(self, mob_engine):
        """Generating a mobile token returns a token string and metadata."""
        from app.main import app

        client = _make_client(mob_engine)
        try:
            resp = client.post(
                "/api/mobile/generate-token",
                json={"device_name": "John's iPhone"},
            )
            assert resp.status_code == 201
            data = resp.json()
            assert data["token"].startswith("de_")
            assert data["token_id"] > 0
            assert "Mobile App" in data["name"]
            assert "John's iPhone" in data["name"]
            assert "created_at" in data
        finally:
            _cleanup(app)

    def test_generate_token_default_device_name(self, mob_engine):
        """A default device name is used if none is provided."""
        from app.main import app

        client = _make_client(mob_engine)
        try:
            resp = client.post("/api/mobile/generate-token", json={})
            assert resp.status_code == 201
            data = resp.json()
            assert "Mobile App" in data["name"]
        finally:
            _cleanup(app)

    def test_generate_token_persisted_in_db(self, mob_engine, mob_session):
        """The generated token is stored in the api_tokens table."""
        from app.main import app

        client = _make_client(mob_engine)
        try:
            resp = client.post(
                "/api/mobile/generate-token",
                json={"device_name": "Test Device"},
            )
            assert resp.status_code == 201
            token_id = resp.json()["token_id"]

            db_token = mob_session.get(ApiToken, token_id)
            assert db_token is not None
            assert db_token.owner_id == _OWNER
            assert "Mobile App" in db_token.name
        finally:
            _cleanup(app)

    def test_generate_token_unauthenticated(self, mob_engine):
        """Unauthenticated requests are rejected with 401."""
        from app.api.mobile import _get_owner_id
        from app.main import app

        Session = sessionmaker(bind=mob_engine)

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

        def _raise_401():
            from fastapi import HTTPException, status

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

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

        client = TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
        try:
            resp = client.post("/api/mobile/generate-token", json={"device_name": "Test"})
            assert resp.status_code == 401
        finally:
            _cleanup(app)


# ---------------------------------------------------------------------------
# Tests – /mobile/register-device
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestRegisterDevice:
    """Tests for POST /api/mobile/register-device."""

    def test_register_new_device(self, mob_engine, mob_session):
        """Registering a new device persists it in mobile_devices."""
        from app.main import app

        client = _make_client(mob_engine)
        try:
            resp = client.post(
                "/api/mobile/register-device",
                json={
                    "push_token": _EXPO_TOKEN,
                    "device_name": "Test iPhone",
                    "platform": "ios",
                },
            )
            assert resp.status_code == 201
            data = resp.json()
            assert data["id"] > 0
            assert data["platform"] == "ios"
            assert data["is_active"] is True
            assert "ExponentPushToken" in data["push_token_preview"]

            device = mob_session.get(MobileDevice, data["id"])
            assert device is not None
            assert device.push_token == _EXPO_TOKEN
            assert device.owner_id == _OWNER
        finally:
            _cleanup(app)

    def test_register_same_token_is_idempotent(self, mob_engine, mob_session):
        """Re-registering the same token reactivates the existing record."""
        from app.main import app

        client = _make_client(mob_engine)
        try:
            resp1 = client.post(
                "/api/mobile/register-device",
                json={"push_token": _EXPO_TOKEN, "platform": "ios"},
            )
            assert resp1.status_code == 201
            id1 = resp1.json()["id"]

            resp2 = client.post(
                "/api/mobile/register-device",
                json={"push_token": _EXPO_TOKEN, "device_name": "Updated Name", "platform": "ios"},
            )
            assert resp2.status_code == 201
            id2 = resp2.json()["id"]

            assert id1 == id2  # Same record reused

            devices = mob_session.query(MobileDevice).filter(MobileDevice.owner_id == _OWNER).all()
            assert len(devices) == 1
        finally:
            _cleanup(app)

    def test_register_invalid_platform(self, mob_engine):
        """An invalid platform value is rejected with 422."""
        from app.main import app

        client = _make_client(mob_engine)
        try:
            resp = client.post(
                "/api/mobile/register-device",
                json={"push_token": _EXPO_TOKEN, "platform": "windows"},
            )
            assert resp.status_code == 422
        finally:
            _cleanup(app)

    def test_register_android_device(self, mob_engine):
        """Android devices can be registered."""
        from app.main import app

        client = _make_client(mob_engine)
        try:
            resp = client.post(
                "/api/mobile/register-device",
                json={
                    "push_token": "ExponentPushToken[android-token-xyz]",
                    "device_name": "Pixel 8",
                    "platform": "android",
                },
            )
            assert resp.status_code == 201
            assert resp.json()["platform"] == "android"
        finally:
            _cleanup(app)


# ---------------------------------------------------------------------------
# Tests – /mobile/devices
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestListDevices:
    """Tests for GET /api/mobile/devices."""

    def test_list_devices_empty(self, mob_engine):
        """An empty list is returned when no devices are registered."""
        from app.main import app

        client = _make_client(mob_engine)
        try:
            resp = client.get("/api/mobile/devices")
            assert resp.status_code == 200
            assert resp.json() == []
        finally:
            _cleanup(app)

    def test_list_devices_returns_own_devices_only(self, mob_engine, mob_session):
        """Only the current user's devices are returned."""
        from app.main import app

        # Add devices for two different owners directly
        mob_session.add(
            MobileDevice(
                owner_id=_OWNER,
                push_token="ExponentPushToken[owner-token-12345]",
                platform="ios",
            )
        )
        mob_session.add(
            MobileDevice(
                owner_id=_OTHER_OWNER,
                push_token="ExponentPushToken[other-token-67890]",
                platform="android",
            )
        )
        mob_session.commit()

        client = _make_client(mob_engine)
        try:
            resp = client.get("/api/mobile/devices")
            assert resp.status_code == 200
            devices = resp.json()
            assert len(devices) == 1
            # The push_token_preview is the first 20 chars + "…"
            assert devices[0]["push_token_preview"].startswith("ExponentPushToken[ow")
        finally:
            _cleanup(app)


# ---------------------------------------------------------------------------
# Tests – DELETE /mobile/devices/{device_id}
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestDeactivateDevice:
    """Tests for DELETE /api/mobile/devices/{device_id}."""

    def test_deactivate_own_device(self, mob_engine, mob_session):
        """Deactivating an active device sets is_active to False (soft-delete, returns 200)."""
        from app.main import app

        device = MobileDevice(
            owner_id=_OWNER,
            push_token=_EXPO_TOKEN,
            platform="ios",
            is_active=True,
        )
        mob_session.add(device)
        mob_session.commit()
        mob_session.refresh(device)
        device_id = device.id

        client = _make_client(mob_engine)
        try:
            resp = client.delete(f"/api/mobile/devices/{device_id}")
            assert resp.status_code == 200
            assert resp.json()["detail"] == "Device deactivated"

            mob_session.expire_all()
            updated = mob_session.get(MobileDevice, device_id)
            assert updated is not None
            assert updated.is_active is False
        finally:
            _cleanup(app)

    def test_delete_inactive_device(self, mob_engine, mob_session):
        """Deleting an already-inactive device permanently removes it (hard-delete, returns 200)."""
        from app.main import app

        device = MobileDevice(
            owner_id=_OWNER,
            push_token=_EXPO_TOKEN,
            platform="ios",
            is_active=False,
        )
        mob_session.add(device)
        mob_session.commit()
        mob_session.refresh(device)
        device_id = device.id

        client = _make_client(mob_engine)
        try:
            resp = client.delete(f"/api/mobile/devices/{device_id}")
            assert resp.status_code == 200
            assert resp.json()["detail"] == "Device deleted"

            mob_session.expire_all()
            deleted = mob_session.get(MobileDevice, device_id)
            assert deleted is None
        finally:
            _cleanup(app)

    def test_deactivate_other_users_device_returns_404(self, mob_engine, mob_session):
        """Attempting to deactivate another user's device returns 404."""
        from app.main import app

        device = MobileDevice(
            owner_id=_OTHER_OWNER,
            push_token="ExponentPushToken[other-token]",
            platform="ios",
            is_active=True,
        )
        mob_session.add(device)
        mob_session.commit()
        mob_session.refresh(device)
        device_id = device.id

        client = _make_client(mob_engine)
        try:
            resp = client.delete(f"/api/mobile/devices/{device_id}")
            assert resp.status_code == 404
        finally:
            _cleanup(app)

    def test_deactivate_nonexistent_device_returns_404(self, mob_engine):
        """Deactivating a device that does not exist returns 404."""
        from app.main import app

        client = _make_client(mob_engine)
        try:
            resp = client.delete("/api/mobile/devices/99999")
            assert resp.status_code == 404
        finally:
            _cleanup(app)


# ---------------------------------------------------------------------------
# Tests – /mobile/whoami
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestWhoAmI:
    """Tests for GET /api/mobile/whoami."""

    def test_whoami_with_no_profile(self, mob_engine):
        """Returns owner_id and inferred email even when no UserProfile exists."""
        from app.main import app

        client = _make_client(mob_engine)
        try:
            resp = client.get("/api/mobile/whoami")
            assert resp.status_code == 200
            data = resp.json()
            assert data["owner_id"] == _OWNER
            assert data["display_name"] is None
            # _OWNER contains "@" so email is inferred from owner_id
            assert data["email"] == _OWNER
            assert data["avatar_url"] is not None  # Gravatar URL from email
            assert data["is_admin"] is False
        finally:
            _cleanup(app)

    def test_whoami_with_profile(self, mob_engine, mob_session):
        """Returns full profile data when a UserProfile record exists."""
        from app.main import app
        from app.models import UserProfile

        profile = UserProfile(
            user_id=_OWNER,
            display_name="Alice Test",
        )
        mob_session.add(profile)
        mob_session.commit()

        client = _make_client(mob_engine)
        try:
            resp = client.get("/api/mobile/whoami")
            assert resp.status_code == 200
            data = resp.json()
            assert data["owner_id"] == _OWNER
            assert data["display_name"] == "Alice Test"
            # owner_id contains "@" so email is inferred from it
            assert data["email"] == _OWNER
            assert data["avatar_url"] is not None  # Gravatar URL
            assert data["is_admin"] is False
            assert data["preferred_language"] is None  # not set yet
        finally:
            _cleanup(app)

    def test_whoami_returns_preferred_language(self, mob_engine, mob_session):
        """preferred_language from UserProfile is included in the whoami response."""
        from app.main import app
        from app.models import UserProfile

        profile = UserProfile(
            user_id=_OWNER,
            display_name="Bob Test",
            preferred_language="de",
        )
        mob_session.add(profile)
        mob_session.commit()

        client = _make_client(mob_engine)
        try:
            resp = client.get("/api/mobile/whoami")
            assert resp.status_code == 200
            data = resp.json()
            assert data["preferred_language"] == "de"
        finally:
            _cleanup(app)

    def test_whoami_no_profile_preferred_language_is_null(self, mob_engine):
        """preferred_language is null when no UserProfile exists."""
        from app.main import app

        client = _make_client(mob_engine)
        try:
            resp = client.get("/api/mobile/whoami")
            assert resp.status_code == 200
            data = resp.json()
            assert data["preferred_language"] is None
        finally:
            _cleanup(app)


# ---------------------------------------------------------------------------
# Tests – push notification utility
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestPushNotificationUtility:
    """Tests for app/utils/push_notification.py."""

    def test_send_expo_push_empty_tokens(self):
        """send_expo_push_notification with no tokens returns empty list."""
        from app.utils.push_notification import send_expo_push_notification

        result = send_expo_push_notification([], "Title", "Body")
        assert result == []

    def test_send_expo_push_calls_expo_api(self):
        """send_expo_push_notification POSTs to the Expo push API."""
        from app.utils.push_notification import send_expo_push_notification

        mock_response = MagicMock()
        mock_response.json.return_value = {"data": [{"status": "ok"}]}
        mock_response.raise_for_status = MagicMock()

        with patch("app.utils.push_notification.httpx.post", return_value=mock_response) as mock_post:
            result = send_expo_push_notification(
                tokens=["ExponentPushToken[abc]"],
                title="Test",
                body="Message",
            )

        mock_post.assert_called_once()
        call_kwargs = mock_post.call_args
        assert "exp.host" in call_kwargs[0][0]
        payload = call_kwargs[1]["json"]
        assert len(payload) == 1
        assert payload[0]["to"] == "ExponentPushToken[abc]"
        assert payload[0]["title"] == "Test"

    def test_send_push_to_owner_no_devices(self, mob_engine):
        """send_push_to_owner silently does nothing when no devices are registered."""
        from app.utils.push_notification import send_push_to_owner

        mock_session = MagicMock()
        mock_session.query.return_value.filter.return_value.all.return_value = []
        mock_session.close = MagicMock()

        with patch("app.utils.push_notification.SessionLocal", return_value=mock_session):
            with patch("app.utils.push_notification.send_expo_push_notification") as mock_send:
                send_push_to_owner("user@example.com", "Title", "Body")
                mock_send.assert_not_called()
                mock_session.close.assert_called_once()

    def test_send_push_to_owner_with_devices(self):
        """send_push_to_owner calls send_expo_push_notification with device tokens."""
        from app.utils.push_notification import send_push_to_owner

        fake_device = MagicMock()
        fake_device.push_token = "ExponentPushToken[device1]"
        fake_device.is_active = True

        mock_session = MagicMock()
        mock_session.query.return_value.filter.return_value.all.return_value = [fake_device]
        mock_session.close = MagicMock()

        with patch("app.utils.push_notification.SessionLocal", return_value=mock_session):
            with patch("app.utils.push_notification.send_expo_push_notification") as mock_send:
                mock_send.return_value = [{"status": "ok"}]
                send_push_to_owner("user@example.com", "Processed!", "Your doc is ready.")

                mock_send.assert_called_once()
                call_kwargs = mock_send.call_args[1]
                assert "ExponentPushToken[device1]" in call_kwargs["tokens"]
