"""Tests for document sharing via expiring links (app/api/shared_links.py)."""

from datetime import datetime, timedelta, timezone

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 FileRecord, SharedLink

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

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


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


@pytest.fixture()
def sl_engine():
    """In-memory SQLite engine with all tables created."""
    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 sl_session(sl_engine):
    """DB session scoped to one test."""
    Session = sessionmaker(bind=sl_engine)
    session = Session()
    yield session
    session.close()


def _make_file(session, owner_id: str = _OWNER, filename: str = "test.pdf") -> FileRecord:
    """Insert a minimal FileRecord and return it."""
    record = FileRecord(
        owner_id=owner_id,
        filehash="abc123",
        original_filename=filename,
        local_filename="/tmp/test.pdf",
        file_size=1024,
        mime_type="application/pdf",
    )
    session.add(record)
    session.commit()
    session.refresh(record)
    return record


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

    Session = sessionmaker(bind=sl_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 – Link CRUD (authenticated)
# ---------------------------------------------------------------------------


class TestCreateSharedLink:
    """Tests for POST /api/shared-links/."""

    @pytest.mark.unit
    def test_create_link_basic(self, sl_engine, sl_session):
        """Creating a link returns the token and share URL."""
        from app.main import app

        file_record = _make_file(sl_session)
        client = _make_client(sl_engine)
        try:
            resp = client.post("/api/shared-links/", json={"file_id": file_record.id})
            assert resp.status_code == 201, resp.text
            data = resp.json()
            assert "token" in data
            assert data["share_url"].startswith("http")
            assert data["token"] in data["share_url"]
            assert data["is_active"] is True
            assert data["has_password"] is False
            assert data["expires_at"] is None
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_create_link_with_expiry(self, sl_engine, sl_session):
        """Creating a link with expires_in_hours sets expires_at."""
        from app.main import app

        file_record = _make_file(sl_session)
        client = _make_client(sl_engine)
        try:
            resp = client.post("/api/shared-links/", json={"file_id": file_record.id, "expires_in_hours": 24})
            assert resp.status_code == 201
            data = resp.json()
            assert data["expires_at"] is not None
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_create_link_with_password(self, sl_engine, sl_session):
        """Creating a link with a password stores a hash (not plaintext)."""
        from app.main import app

        file_record = _make_file(sl_session)
        client = _make_client(sl_engine)
        try:
            resp = client.post("/api/shared-links/", json={"file_id": file_record.id, "password": "secret"})
            assert resp.status_code == 201
            data = resp.json()
            assert data["has_password"] is True
            # Verify plaintext is not stored.
            from app.models import SharedLink

            Session = sessionmaker(bind=sl_engine)
            with Session() as sess:
                db_link = sess.query(SharedLink).filter(SharedLink.token == data["token"]).first()
                assert db_link is not None
                assert db_link.password_hash != "secret"
                # Hash format is "{salt_hex}:{dk_hex}" — check it can verify correctly.
                assert ":" in db_link.password_hash
                from app.api.shared_links import _verify_password

                assert _verify_password("secret", db_link.password_hash) is True
                assert _verify_password("wrong", db_link.password_hash) is False
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_create_link_with_max_views(self, sl_engine, sl_session):
        """Creating a link with max_views stores the limit."""
        from app.main import app

        file_record = _make_file(sl_session)
        client = _make_client(sl_engine)
        try:
            resp = client.post("/api/shared-links/", json={"file_id": file_record.id, "max_views": 5})
            assert resp.status_code == 201
            assert resp.json()["max_views"] == 5
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_create_link_wrong_file_returns_404(self, sl_engine, sl_session):
        """Creating a link for a non-existent file returns 404."""
        from app.main import app

        client = _make_client(sl_engine)
        try:
            resp = client.post("/api/shared-links/", json={"file_id": 99999})
            assert resp.status_code == 404
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_create_link_other_users_file_returns_404(self, sl_engine, sl_session):
        """Creating a link for another user's file returns 404."""
        from app.main import app

        other_file = _make_file(sl_session, owner_id=_OTHER_OWNER)
        client = _make_client(sl_engine, _OWNER)
        try:
            from app.config import settings

            original = settings.multi_user_enabled
            settings.multi_user_enabled = True
            try:
                resp = client.post("/api/shared-links/", json={"file_id": other_file.id})
                assert resp.status_code == 404
            finally:
                settings.multi_user_enabled = original
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_create_link_with_label(self, sl_engine, sl_session):
        """A label is returned when provided."""
        from app.main import app

        file_record = _make_file(sl_session)
        client = _make_client(sl_engine)
        try:
            resp = client.post("/api/shared-links/", json={"file_id": file_record.id, "label": "For Alice"})
            assert resp.status_code == 201
            assert resp.json()["label"] == "For Alice"
        finally:
            _cleanup(app)


class TestListSharedLinks:
    """Tests for GET /api/shared-links/."""

    @pytest.mark.unit
    def test_list_empty(self, sl_engine):
        """Listing when no links exist returns an empty list."""
        from app.main import app

        client = _make_client(sl_engine)
        try:
            resp = client.get("/api/shared-links/")
            assert resp.status_code == 200
            assert resp.json() == []
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_list_returns_own_links(self, sl_engine, sl_session):
        """Listing returns all links created by the current user."""
        from app.main import app

        file_record = _make_file(sl_session)
        client = _make_client(sl_engine)
        try:
            client.post("/api/shared-links/", json={"file_id": file_record.id, "label": "Link A"})
            client.post("/api/shared-links/", json={"file_id": file_record.id, "label": "Link B"})
            resp = client.get("/api/shared-links/")
            assert resp.status_code == 200
            assert len(resp.json()) == 2
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_list_isolation(self, sl_engine, sl_session):
        """Users can only list their own links."""
        from app.main import app

        file_a = _make_file(sl_session, owner_id=_OWNER)
        client_a = _make_client(sl_engine, _OWNER)
        try:
            client_a.post("/api/shared-links/", json={"file_id": file_a.id})
        finally:
            _cleanup(app)

        file_b = _make_file(sl_session, owner_id=_OTHER_OWNER)
        client_b = _make_client(sl_engine, _OTHER_OWNER)
        try:
            resp = client_b.get("/api/shared-links/")
            assert resp.status_code == 200
            assert resp.json() == []
        finally:
            _cleanup(app)


class TestRevokeSharedLink:
    """Tests for DELETE /api/shared-links/{id}."""

    @pytest.mark.unit
    def test_revoke_link(self, sl_engine, sl_session):
        """Revoking a link marks it inactive."""
        from app.main import app

        file_record = _make_file(sl_session)
        client = _make_client(sl_engine)
        try:
            create_resp = client.post("/api/shared-links/", json={"file_id": file_record.id})
            link_id = create_resp.json()["id"]

            resp = client.delete(f"/api/shared-links/{link_id}")
            assert resp.status_code == 200

            list_resp = client.get("/api/shared-links/")
            revoked = [link for link in list_resp.json() if link["id"] == link_id][0]
            assert revoked["is_active"] is False
            assert revoked["revoked_at"] is not None
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_revoke_already_revoked(self, sl_engine, sl_session):
        """Revoking an already-revoked link returns 400."""
        from app.main import app

        file_record = _make_file(sl_session)
        client = _make_client(sl_engine)
        try:
            create_resp = client.post("/api/shared-links/", json={"file_id": file_record.id})
            link_id = create_resp.json()["id"]
            client.delete(f"/api/shared-links/{link_id}")
            resp = client.delete(f"/api/shared-links/{link_id}")
            assert resp.status_code == 400
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_revoke_nonexistent(self, sl_engine):
        """Revoking a non-existent link returns 404."""
        from app.main import app

        client = _make_client(sl_engine)
        try:
            resp = client.delete("/api/shared-links/99999")
            assert resp.status_code == 404
        finally:
            _cleanup(app)

    @pytest.mark.unit
    def test_revoke_other_users_link(self, sl_engine, sl_session):
        """A user cannot revoke another user's link."""
        from app.main import app

        file_a = _make_file(sl_session, owner_id=_OWNER)
        client_a = _make_client(sl_engine, _OWNER)
        try:
            create_resp = client_a.post("/api/shared-links/", json={"file_id": file_a.id})
            link_id = create_resp.json()["id"]
        finally:
            _cleanup(app)

        client_b = _make_client(sl_engine, _OTHER_OWNER)
        try:
            resp = client_b.delete(f"/api/shared-links/{link_id}")
            assert resp.status_code == 404
        finally:
            _cleanup(app)


# ---------------------------------------------------------------------------
# Tests – Public endpoints
# ---------------------------------------------------------------------------


class TestPublicInfo:
    """Tests for GET /api/share/{token}/info."""

    @pytest.mark.unit
    def test_info_for_valid_link(self, sl_engine, sl_session):
        """Public info endpoint returns metadata for a valid link."""
        from app.main import app

        file_record = _make_file(sl_session, filename="report.pdf")
        client = _make_client(sl_engine)
        try:
            create_resp = client.post(
                "/api/shared-links/",
                json={"file_id": file_record.id, "label": "For Bob"},
            )
            token = create_resp.json()["token"]
        finally:
            _cleanup(app)

        # Public client (no auth override)
        from app.main import app as main_app

        Session = sessionmaker(bind=sl_engine)

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

        main_app.dependency_overrides[get_db] = _override_db
        pub_client = TestClient(main_app, base_url="http://localhost", raise_server_exceptions=False)
        try:
            resp = pub_client.get(f"/api/share/{token}/info")
            assert resp.status_code == 200
            data = resp.json()
            assert data["token"] == token
            assert data["is_valid"] is True
            assert data["original_filename"] == "report.pdf"
            assert data["label"] == "For Bob"
            assert data["has_password"] is False
        finally:
            main_app.dependency_overrides.clear()

    @pytest.mark.unit
    def test_info_for_unknown_token(self, sl_engine):
        """Unknown token returns 404."""
        from app.main import app

        Session = sessionmaker(bind=sl_engine)

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

        app.dependency_overrides[get_db] = _override_db
        client = TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
        try:
            resp = client.get("/api/share/nonexistenttoken/info")
            assert resp.status_code == 404
        finally:
            app.dependency_overrides.clear()

    @pytest.mark.unit
    def test_info_shows_has_password(self, sl_engine, sl_session):
        """Info for a password-protected link shows has_password=True."""
        from app.main import app

        file_record = _make_file(sl_session)
        client = _make_client(sl_engine)
        try:
            create_resp = client.post(
                "/api/shared-links/",
                json={"file_id": file_record.id, "password": "hunter2"},
            )
            token = create_resp.json()["token"]
        finally:
            _cleanup(app)

        Session = sessionmaker(bind=sl_engine)

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

        app.dependency_overrides[get_db] = _override_db
        pub_client = TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
        try:
            resp = pub_client.get(f"/api/share/{token}/info")
            assert resp.status_code == 200
            assert resp.json()["has_password"] is True
        finally:
            app.dependency_overrides.clear()

    @pytest.mark.unit
    def test_expired_link_shows_invalid(self, sl_engine, sl_session):
        """An expired link reports is_valid=False in info response."""
        now = datetime.now(timezone.utc)
        past = now - timedelta(hours=1)
        link = SharedLink(
            token="expiredtoken123",
            file_id=_make_file(sl_session).id,
            owner_id=_OWNER,
            expires_at=past,
            view_count=0,
            is_active=True,
        )
        sl_session.add(link)
        sl_session.commit()

        from app.main import app

        Session = sessionmaker(bind=sl_engine)

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

        app.dependency_overrides[get_db] = _override_db
        client = TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
        try:
            resp = client.get("/api/share/expiredtoken123/info")
            assert resp.status_code == 200
            assert resp.json()["is_valid"] is False
        finally:
            app.dependency_overrides.clear()

    @pytest.mark.unit
    def test_view_limit_reached_shows_invalid(self, sl_engine, sl_session):
        """A link that has reached its view limit reports is_valid=False."""
        link = SharedLink(
            token="limitedtoken456",
            file_id=_make_file(sl_session).id,
            owner_id=_OWNER,
            max_views=3,
            view_count=3,
            is_active=True,
        )
        sl_session.add(link)
        sl_session.commit()

        from app.main import app

        Session = sessionmaker(bind=sl_engine)

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

        app.dependency_overrides[get_db] = _override_db
        client = TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
        try:
            resp = client.get("/api/share/limitedtoken456/info")
            assert resp.status_code == 200
            assert resp.json()["is_valid"] is False
        finally:
            app.dependency_overrides.clear()


class TestPublicDownload:
    """Tests for GET /api/share/{token}/download."""

    @pytest.mark.unit
    def test_download_no_file_on_disk_returns_404(self, sl_engine, sl_session):
        """Download endpoint returns 404 when file not on disk."""
        link = SharedLink(
            token="dltoken001",
            file_id=_make_file(sl_session, filename="missing.pdf").id,
            owner_id=_OWNER,
            view_count=0,
            is_active=True,
        )
        sl_session.add(link)
        sl_session.commit()

        from app.main import app

        Session = sessionmaker(bind=sl_engine)

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

        app.dependency_overrides[get_db] = _override_db
        client = TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
        try:
            resp = client.get("/api/share/dltoken001/download")
            assert resp.status_code == 404
        finally:
            app.dependency_overrides.clear()

    @pytest.mark.unit
    def test_download_expired_returns_410(self, sl_engine, sl_session):
        """Download endpoint returns 410 when link has expired."""
        past = datetime.now(timezone.utc) - timedelta(hours=1)
        link = SharedLink(
            token="expiredlink999",
            file_id=_make_file(sl_session).id,
            owner_id=_OWNER,
            expires_at=past,
            view_count=0,
            is_active=True,
        )
        sl_session.add(link)
        sl_session.commit()

        from app.main import app

        Session = sessionmaker(bind=sl_engine)

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

        app.dependency_overrides[get_db] = _override_db
        client = TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
        try:
            resp = client.get("/api/share/expiredlink999/download")
            assert resp.status_code == 410
        finally:
            app.dependency_overrides.clear()

    @pytest.mark.unit
    def test_download_revoked_returns_410(self, sl_engine, sl_session):
        """Download endpoint returns 410 when link has been revoked."""
        link = SharedLink(
            token="revokedlink777",
            file_id=_make_file(sl_session).id,
            owner_id=_OWNER,
            view_count=0,
            is_active=False,
            revoked_at=datetime.now(timezone.utc),
        )
        sl_session.add(link)
        sl_session.commit()

        from app.main import app

        Session = sessionmaker(bind=sl_engine)

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

        app.dependency_overrides[get_db] = _override_db
        client = TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
        try:
            resp = client.get("/api/share/revokedlink777/download")
            assert resp.status_code == 410
        finally:
            app.dependency_overrides.clear()

    @pytest.mark.unit
    def test_download_password_required(self, sl_engine, sl_session):
        """GET download endpoint returns 401 when password is required but not supplied."""
        from app.api.shared_links import _hash_password

        link = SharedLink(
            token="pwdlink111",
            file_id=_make_file(sl_session).id,
            owner_id=_OWNER,
            password_hash=_hash_password("correct"),
            view_count=0,
            is_active=True,
        )
        sl_session.add(link)
        sl_session.commit()

        from app.main import app

        Session = sessionmaker(bind=sl_engine)

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

        app.dependency_overrides[get_db] = _override_db
        client = TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
        try:
            # GET without password returns 401
            resp = client.get("/api/share/pwdlink111/download")
            assert resp.status_code == 401
        finally:
            app.dependency_overrides.clear()

    @pytest.mark.unit
    def test_download_wrong_password(self, sl_engine, sl_session):
        """POST download endpoint returns 403 for wrong password."""
        from app.api.shared_links import _hash_password

        link = SharedLink(
            token="pwdlink222",
            file_id=_make_file(sl_session).id,
            owner_id=_OWNER,
            password_hash=_hash_password("correct"),
            view_count=0,
            is_active=True,
        )
        sl_session.add(link)
        sl_session.commit()

        from app.main import app

        Session = sessionmaker(bind=sl_engine)

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

        app.dependency_overrides[get_db] = _override_db
        client = TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
        try:
            # Password is supplied via POST body, not URL query param.
            resp = client.post("/api/share/pwdlink222/download", json={"password": "wrong"})
            assert resp.status_code == 403
        finally:
            app.dependency_overrides.clear()

    @pytest.mark.unit
    def test_download_view_limit_reached(self, sl_engine, sl_session):
        """Download endpoint returns 410 when view limit is already reached."""
        link = SharedLink(
            token="limitedlink333",
            file_id=_make_file(sl_session).id,
            owner_id=_OWNER,
            max_views=2,
            view_count=2,
            is_active=True,
        )
        sl_session.add(link)
        sl_session.commit()

        from app.main import app

        Session = sessionmaker(bind=sl_engine)

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

        app.dependency_overrides[get_db] = _override_db
        client = TestClient(app, base_url="http://localhost", raise_server_exceptions=False)
        try:
            resp = client.get("/api/share/limitedlink333/download")
            assert resp.status_code == 410
        finally:
            app.dependency_overrides.clear()


# ---------------------------------------------------------------------------
# Tests – Helper utilities
# ---------------------------------------------------------------------------


class TestHelpers:
    """Tests for internal helper functions."""

    @pytest.mark.unit
    def test_generate_token_unique(self):
        """Generated tokens should be unique."""
        from app.api.shared_links import _generate_token

        tokens = {_generate_token() for _ in range(100)}
        assert len(tokens) == 100

    @pytest.mark.unit
    def test_hash_password_deterministic(self):
        """Two calls with the same password produce different hashes (random salt)."""
        from app.api.shared_links import _hash_password, _verify_password

        h1 = _hash_password("mysecret")
        h2 = _hash_password("mysecret")
        # Salt is random, so hashes differ — but both verify correctly.
        assert h1 != h2
        assert _verify_password("mysecret", h1) is True
        assert _verify_password("mysecret", h2) is True
        # Format is "{salt_hex}:{dk_hex}"
        assert ":" in h1

    @pytest.mark.unit
    def test_verify_password_correct(self):
        """_verify_password returns True for a matching password."""
        from app.api.shared_links import _hash_password, _verify_password

        h = _hash_password("correct")
        assert _verify_password("correct", h) is True

    @pytest.mark.unit
    def test_verify_password_wrong(self):
        """_verify_password returns False for a wrong password."""
        from app.api.shared_links import _hash_password, _verify_password

        h = _hash_password("correct")
        assert _verify_password("wrong", h) is False

    @pytest.mark.unit
    def test_is_link_valid_active(self):
        """_is_link_valid returns True for a basic active link."""
        from app.api.shared_links import _is_link_valid

        link = SharedLink(is_active=True, view_count=0)
        assert _is_link_valid(link) is True

    @pytest.mark.unit
    def test_is_link_valid_revoked(self):
        """_is_link_valid returns False for a revoked link."""
        from app.api.shared_links import _is_link_valid

        link = SharedLink(is_active=False, view_count=0)
        assert _is_link_valid(link) is False

    @pytest.mark.unit
    def test_is_link_valid_expired(self):
        """_is_link_valid returns False when expires_at is in the past."""
        from app.api.shared_links import _is_link_valid

        past = datetime.now(timezone.utc) - timedelta(seconds=1)
        link = SharedLink(is_active=True, expires_at=past, view_count=0)
        assert _is_link_valid(link) is False

    @pytest.mark.unit
    def test_is_link_valid_not_yet_expired(self):
        """_is_link_valid returns True when expires_at is in the future."""
        from app.api.shared_links import _is_link_valid

        future = datetime.now(timezone.utc) + timedelta(hours=1)
        link = SharedLink(is_active=True, expires_at=future, view_count=0)
        assert _is_link_valid(link) is True

    @pytest.mark.unit
    def test_is_link_valid_view_limit_hit(self):
        """_is_link_valid returns False when view_count >= max_views."""
        from app.api.shared_links import _is_link_valid

        link = SharedLink(is_active=True, max_views=3, view_count=3)
        assert _is_link_valid(link) is False

    @pytest.mark.unit
    def test_is_link_valid_view_limit_not_hit(self):
        """_is_link_valid returns True when view_count < max_views."""
        from app.api.shared_links import _is_link_valid

        link = SharedLink(is_active=True, max_views=3, view_count=2)
        assert _is_link_valid(link) is True
