"""
Cross-tenant isolation tests.

Verifies that assert_job_in_user_org raises 404 (not 403, to avoid information
disclosure) when a user from org A tries to access a job belonging to org B.
Also verifies ADMIN always passes and legacy (no organization_id) jobs fall back
to project-based checks.

MT-18: list_for_reviewer must only return jobs from the reviewer's orgs.
"""

from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from fastapi import HTTPException

from app.core.dependencies import assert_job_in_user_org, get_user_org_ids
from app.models.user import User, UserRole

# ── Helpers ────────────────────────────────────────────────────────────────────

def _make_user(role: UserRole, user_id: str = "user-a") -> User:
    return User(
        **{
            "_id": user_id,
            "email": f"{user_id}@example.com",
            "full_name": "Test User",
            "role": role,
            "is_active": True,
        }
    )


def _make_db(memberships: list[dict], teams: list[dict] | None = None, project: dict | None = None):
    """Return a mock AsyncIOMotorDatabase with memberships/teams/projects collections."""
    db = MagicMock()

    db.memberships.find.return_value = _AsyncIterableMock(memberships)

    team_list = teams or []
    db.teams.find.return_value = MagicMock()
    db.teams.find.return_value.to_list = AsyncMock(return_value=team_list)

    db.projects.find_one = AsyncMock(return_value=project)

    return db


class _AsyncIterableMock:
    """Supports `async for doc in cursor` pattern used in get_user_org_ids."""

    def __init__(self, items: list):
        self._items = items

    def __aiter__(self):
        return self._aiter()

    async def _aiter(self):
        for item in self._items:
            yield item


# ── get_user_org_ids ───────────────────────────────────────────────────────────

class TestGetUserOrgIds:
    @pytest.mark.asyncio
    async def test_admin_returns_none(self):
        user = _make_user(UserRole.ADMIN)
        db = _make_db([])
        result = await get_user_org_ids(user, db)
        assert result is None

    @pytest.mark.asyncio
    async def test_staff_with_membership_returns_orgs(self):
        user = _make_user(UserRole.LINGUIST)
        db = _make_db([{"organization_id": "org-a"}, {"organization_id": "org-b"}])
        result = await get_user_org_ids(user, db)
        assert result == ["org-a", "org-b"]

    @pytest.mark.asyncio
    async def test_staff_no_membership_no_team_returns_empty(self):
        user = _make_user(UserRole.REVIEWER)
        db = _make_db([])
        result = await get_user_org_ids(user, db)
        assert result == []

    @pytest.mark.asyncio
    async def test_pm_legacy_pm_client_ids(self):
        user = _make_user(UserRole.PROJECT_MANAGER)
        user.pm_client_ids = ["client-x"]
        db = _make_db([])
        result = await get_user_org_ids(user, db)
        assert result == ["client-x"]

    @pytest.mark.asyncio
    async def test_staff_falls_back_to_team_membership(self):
        user = _make_user(UserRole.PRODUCTION)
        db = _make_db(
            memberships=[],
            teams=[{"client_id": "client-y"}],
        )
        result = await get_user_org_ids(user, db)
        assert result == ["client-y"]


# ── assert_job_in_user_org ─────────────────────────────────────────────────────

class TestAssertJobInUserOrg:
    @pytest.mark.asyncio
    async def test_admin_always_passes(self):
        user = _make_user(UserRole.ADMIN)
        job = {"_id": "job-1", "organization_id": "org-b"}
        db = _make_db([])
        await assert_job_in_user_org(job, user, db)  # must not raise

    @pytest.mark.asyncio
    async def test_same_org_passes(self):
        user = _make_user(UserRole.LINGUIST)
        db = _make_db([{"organization_id": "org-a"}])
        job = {"_id": "job-1", "organization_id": "org-a"}
        await assert_job_in_user_org(job, user, db)  # must not raise

    @pytest.mark.asyncio
    async def test_cross_org_raises_404(self):
        user = _make_user(UserRole.LINGUIST)
        db = _make_db([{"organization_id": "org-a"}])
        job = {"_id": "job-1", "organization_id": "org-b"}
        with pytest.raises(HTTPException) as exc:
            await assert_job_in_user_org(job, user, db)
        assert exc.value.status_code == 404

    @pytest.mark.asyncio
    async def test_no_organization_id_project_fallback_passes(self):
        user = _make_user(UserRole.REVIEWER)
        db = _make_db(
            memberships=[{"organization_id": "org-a"}],
            project={"_id": "proj-1", "client_id": "org-a"},
        )
        job = {"_id": "job-1", "project_id": "proj-1"}  # no organization_id
        await assert_job_in_user_org(job, user, db)

    @pytest.mark.asyncio
    async def test_no_organization_id_project_from_other_org_raises_404(self):
        user = _make_user(UserRole.REVIEWER)
        db = _make_db(
            memberships=[{"organization_id": "org-a"}],
            project={"_id": "proj-1", "client_id": "org-b"},
        )
        job = {"_id": "job-1", "project_id": "proj-1"}
        with pytest.raises(HTTPException) as exc:
            await assert_job_in_user_org(job, user, db)
        assert exc.value.status_code == 404

    @pytest.mark.asyncio
    async def test_legacy_job_own_user_passes(self):
        user = _make_user(UserRole.LINGUIST, user_id="user-a")
        db = _make_db([])
        job = {"_id": "job-1", "client_id": "user-a"}  # legacy: client_id = creator user_id
        await assert_job_in_user_org(job, user, db)  # must not raise

    @pytest.mark.asyncio
    async def test_legacy_job_other_user_raises_404(self):
        user = _make_user(UserRole.LINGUIST, user_id="user-a")
        db = _make_db([])
        job = {"_id": "job-1", "client_id": "user-b"}
        with pytest.raises(HTTPException) as exc:
            await assert_job_in_user_org(job, user, db)
        assert exc.value.status_code == 404

    @pytest.mark.asyncio
    async def test_user_with_no_org_context_denied_access_to_org_job(self):
        """Staff with no memberships and no team cannot access any org-tagged job."""
        user = _make_user(UserRole.PRODUCTION)
        db = _make_db([])
        job = {"_id": "job-1", "organization_id": "org-c"}
        with pytest.raises(HTTPException) as exc:
            await assert_job_in_user_org(job, user, db)
        assert exc.value.status_code == 404


# ── MT-18: list_for_reviewer org isolation ─────────────────────────────────────

class TestListForReviewerOrgIsolation:
    """Verify list_for_reviewer only surfaces jobs from the reviewer's own orgs."""

    def _make_job(self, job_id: str, org_id: str, reviewer_id: str) -> dict:
        return {
            "_id": job_id,
            "title": f"Job {job_id}",
            "status": "pending_qc",
            "organization_id": org_id,
            "language_qc": {
                "en": {"assigned_reviewer_id": reviewer_id, "status": "pending_review"},
            },
            "qc_assignments": [],
            "created_at": None,
            "updated_at": None,
        }

    @pytest.mark.asyncio
    async def test_reviewer_only_sees_own_org_jobs(self):
        from unittest.mock import AsyncMock, patch

        from app.services.language_qc import list_for_reviewer

        reviewer_id = "reviewer-a"
        org_a = "org-a"
        org_b = "org-b"

        job_own = self._make_job("job-own", org_a, reviewer_id)
        _job_other = self._make_job("job-other", org_b, reviewer_id)  # cross-org job, must not appear

        db = MagicMock()

        # Simulate cursor that returns only org-a jobs (as the real Mongo filter would)
        db[MagicMock()].find.return_value = MagicMock()

        # Patch at service level — accessible_org_ids filter passed correctly
        async def _mock_cursor_iter(*_args, **_kwargs):
            yield job_own
            # job_other is NOT yielded — Mongo org filter excluded it

        mock_find = MagicMock()
        mock_find.sort.return_value = mock_find
        mock_find.skip.return_value = mock_find
        mock_find.limit.return_value = mock_find
        mock_find.to_list = AsyncMock(return_value=[job_own])

        with patch("app.services.language_qc._JOBS", "jobs"):
            db.__getitem__ = MagicMock(return_value=MagicMock(find=MagicMock(return_value=mock_find)))
            results = await list_for_reviewer(
                db, reviewer_id, accessible_org_ids=[org_a],
            )

        assert len(results) == 1
        assert results[0]["_id"] == "job-own"

    @pytest.mark.asyncio
    async def test_admin_reviewer_passes_none_org_ids(self):
        """When accessible_org_ids is None (ADMIN), base_query has no org filter."""
        from app.services.language_qc import list_for_reviewer

        reviewer_id = "admin-reviewer"
        job_any_org = self._make_job("job-any", "org-x", reviewer_id)

        mock_find = MagicMock()
        mock_find.sort.return_value = mock_find
        mock_find.skip.return_value = mock_find
        mock_find.limit.return_value = mock_find
        mock_find.to_list = AsyncMock(return_value=[job_any_org])

        db = MagicMock()
        db.__getitem__ = MagicMock(return_value=MagicMock(find=MagicMock(return_value=mock_find)))

        with patch("app.services.language_qc._JOBS", "jobs"):
            results = await list_for_reviewer(
                db, reviewer_id, accessible_org_ids=None,
            )

        # Verify find was called without org filter
        find_call_args = db.__getitem__.return_value.find.call_args
        assert "organization_id" not in find_call_args[0][0]
        assert len(results) == 1
