"""Regression tests for the tenant-isolation findings in docs/security.md.

Each test reproduces one documented attack path and asserts it is now refused.
The four marked "probe result" in that document were reproduced against the
running API before the fix; the rest were confirmed by code review.
"""

import pytest
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient

from apps.companies.models import Organization, OrganizationMembership

User = get_user_model()


def client_for(user):
    client = APIClient()
    client.force_authenticate(user)
    return client


def grant(user, organization, *codenames):
    """Give `user` a role carrying `codenames` so the legacy RBAC menu gate
    (HasMenuPermission) lets the request reach the code under test."""
    from apps.core.models import Permission, Role, UserRole

    role = Role.objects.create(name=f"Role {user.email}", slug=f"role-{user.pk}")
    for codename in codenames:
        domain, action = codename.split(".", 1)
        permission, _ = Permission.objects.get_or_create(
            domain=domain, action=action, defaults={"label": codename}
        )
        role.permissions.add(permission)
    UserRole.objects.create(user=user, role=role, organization=organization)
    return role


@pytest.fixture
def two_orgs(db):
    from apps.hr.signals import ensure_employee_stub

    org_a = Organization.objects.create(name="Sec Org A")
    org_b = Organization.objects.create(name="Sec Org B")
    user_a = User.objects.create_user(email="a@sec.test", password="p")
    user_b = User.objects.create_user(email="b@sec.test", password="p")
    OrganizationMembership.objects.create(organization=org_a, user=user_a)
    OrganizationMembership.objects.create(organization=org_b, user=user_b)
    ensure_employee_stub(user_a)
    ensure_employee_stub(user_b)
    return org_a, org_b, user_a, user_b


@pytest.mark.django_db
class TestSec01UserDirectoryScoping:
    def test_cannot_retrieve_user_from_another_org(self, two_orgs):
        _, _, user_a, user_b = two_orgs
        resp = client_for(user_a).get(f"/api/users/{user_b.id}/")
        assert resp.status_code == 404

    def test_list_excludes_users_from_another_org(self, two_orgs):
        _, _, user_a, user_b = two_orgs
        resp = client_for(user_a).get("/api/users/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        assert str(user_b.id) not in {str(row["id"]) for row in rows}

    def test_me_still_works(self, two_orgs):
        _, _, user_a, _ = two_orgs
        resp = client_for(user_a).get("/api/users/me/")
        assert resp.status_code == 200
        assert resp.data["email"] == user_a.email


@pytest.mark.django_db
class TestSec02CommentTenantAuthorization:
    def _comment_on(self, target, author):
        from django.contrib.contenttypes.models import ContentType

        from apps.core.models import Comment

        return Comment.objects.create(
            content_type=ContentType.objects.get_for_model(type(target)),
            object_id=str(target.pk),
            content="private note",
            author=author,
        )

    def test_cannot_read_comment_on_another_orgs_object(self, two_orgs):
        from apps.companies.models import Company

        org_a, org_b, user_a, user_b = two_orgs
        target = Company.objects.create(organization=org_b, name="B Co")
        self._comment_on(target, user_b)

        resp = client_for(user_a).get(
            "/api/comments/", {"model": "companies.company", "object_id": str(target.pk)}
        )
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        assert rows == []

    def test_can_read_comment_on_own_orgs_object(self, two_orgs):
        from apps.companies.models import Company

        org_a, _, user_a, _ = two_orgs
        target = Company.objects.create(organization=org_a, name="A Co")
        self._comment_on(target, user_a)

        resp = client_for(user_a).get(
            "/api/comments/", {"model": "companies.company", "object_id": str(target.pk)}
        )
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        assert len(rows) == 1

    def test_cannot_create_comment_on_another_orgs_object(self, two_orgs):
        from apps.companies.models import Company

        _, org_b, user_a, _ = two_orgs
        target = Company.objects.create(organization=org_b, name="B Co")

        resp = client_for(user_a).post(
            "/api/comments/",
            {"model": "companies.company", "object_id": str(target.pk), "content": "hi"},
            format="json",
        )
        assert resp.status_code == 403


@pytest.mark.django_db
class TestSec03ChatParticipantScoping:
    def test_cannot_add_user_from_another_org_to_group(self, two_orgs):
        _, _, user_a, user_b = two_orgs
        resp = client_for(user_a).post(
            "/api/chat/conversations/",
            {"kind": "group", "name": "Leaky", "participant_ids": [str(user_b.id)]},
            format="json",
        )
        assert resp.status_code == 400
        assert "participant_ids" in resp.data

    def test_can_add_user_from_same_org(self, two_orgs):
        from apps.hr.signals import ensure_employee_stub

        org_a, _, user_a, _ = two_orgs
        peer = User.objects.create_user(email="peer@sec.test", password="p")
        OrganizationMembership.objects.create(organization=org_a, user=peer)
        ensure_employee_stub(peer)

        resp = client_for(user_a).post(
            "/api/chat/conversations/",
            {"kind": "group", "name": "Fine", "participant_ids": [str(peer.id)]},
            format="json",
        )
        assert resp.status_code == 201, resp.content


@pytest.mark.django_db
class TestSec04OrgProfileWriteAuthorization:
    def test_ordinary_member_cannot_update_org_profile(self, two_orgs):
        org_a, _, user_a, _ = two_orgs
        resp = client_for(user_a).patch(
            "/api/org-profile/current/", {"tagline": "pwned"}, format="json"
        )
        assert resp.status_code == 403
        org_a.refresh_from_db()
        assert org_a.tagline != "pwned"

    def test_director_can_update_org_profile(self, two_orgs):
        org_a, _, user_a, _ = two_orgs
        user_a.organization_role = "director"
        user_a.save(update_fields=["organization_role"])

        resp = client_for(user_a).patch(
            "/api/org-profile/current/", {"tagline": "legit"}, format="json"
        )
        assert resp.status_code == 200, resp.content
        org_a.refresh_from_db()
        assert org_a.tagline == "legit"

    def test_anonymous_get_still_public(self, two_orgs):
        resp = APIClient().get("/api/org-profile/current/")
        assert resp.status_code == 200


@pytest.mark.django_db
class TestSec05TaskRelationScoping:
    def test_cannot_assign_task_to_user_in_another_org(self, two_orgs):
        org_a, _, user_a, user_b = two_orgs
        grant(user_a, org_a, "tasks.create")
        resp = client_for(user_a).post(
            "/api/tasks/",
            {"title": "Cross-tenant", "assignee": str(user_b.id)},
            format="json",
        )
        assert resp.status_code == 400
        assert "assignee" in resp.data

    def test_cannot_link_task_to_another_orgs_project(self, two_orgs):
        from apps.projects.models import Project

        org_a, org_b, user_a, _ = two_orgs
        grant(user_a, org_a, "tasks.create")
        project = Project.objects.create(organization=org_b, name="B Project")

        resp = client_for(user_a).post(
            "/api/tasks/",
            {"title": "Cross-tenant", "project": str(project.id)},
            format="json",
        )
        assert resp.status_code == 400
        assert "project" in resp.data


@pytest.mark.django_db
class TestSec08WorkflowFieldAuthorization:
    def test_requester_cannot_approve_own_reimbursement(self, two_orgs):
        from apps.administrations.models import Reimbursement

        org_a, _, user_a, _ = two_orgs
        # Enough menu permission to edit own request — but no full scope, which
        # is what the approval decision requires.
        grant(user_a, org_a, "administrations.update")
        record = Reimbursement.objects.create(
            organization=org_a, requester=user_a, title="Taxi", amount="100.00",
        )

        resp = client_for(user_a).patch(
            f"/api/reimbursements/{record.id}/", {"status": "approved"}, format="json"
        )
        assert resp.status_code == 400
        assert "status" in resp.data
        record.refresh_from_db()
        assert record.status != "approved"

    def test_requester_cannot_forge_approver_or_paid_at(self, two_orgs):
        from apps.administrations.models import PaymentRequest

        org_a, _, user_a, _ = two_orgs
        grant(user_a, org_a, "administrations.update")
        record = PaymentRequest.objects.create(
            organization=org_a, requester=user_a, title="Invoice", amount="100.00",
        )

        resp = client_for(user_a).patch(
            f"/api/payment-requests/{record.id}/",
            {"approved_by": str(user_a.id), "paid_at": "2026-01-01T00:00:00Z"},
            format="json",
        )
        assert resp.status_code == 400
        record.refresh_from_db()
        assert record.approved_by_id is None
        assert record.paid_at is None

    def test_requester_may_still_edit_descriptive_fields(self, two_orgs):
        from apps.administrations.models import Reimbursement

        org_a, _, user_a, _ = two_orgs
        grant(user_a, org_a, "administrations.update")
        record = Reimbursement.objects.create(
            organization=org_a, requester=user_a, title="Taxi", amount="100.00",
        )

        resp = client_for(user_a).patch(
            f"/api/reimbursements/{record.id}/", {"title": "Taxi to airport"}, format="json"
        )
        assert resp.status_code == 200, resp.content
        record.refresh_from_db()
        assert record.title == "Taxi to airport"

    def test_approver_decision_is_stamped_with_the_caller(self, two_orgs):
        from apps.administrations.models import Reimbursement

        org_a, _, user_a, _ = two_orgs
        requester = User.objects.create_user(email="req@sec.test", password="p")
        OrganizationMembership.objects.create(organization=org_a, user=requester)
        record = Reimbursement.objects.create(
            organization=org_a, requester=requester, title="Taxi", amount="100.00",
        )
        # Director has full administrations scope.
        user_a.organization_role = "director"
        user_a.save(update_fields=["organization_role"])

        resp = client_for(user_a).patch(
            f"/api/reimbursements/{record.id}/",
            # Claim someone else approved it; the server must ignore that.
            {"status": "approved", "approved_by": str(requester.id)},
            format="json",
        )
        assert resp.status_code == 200, resp.content
        record.refresh_from_db()
        assert record.status == "approved"
        assert record.approved_by_id == user_a.id


@pytest.mark.django_db
class TestSec12AccountStatusEnforcement:
    def test_suspended_account_cannot_log_in(self, two_orgs):
        _, _, user_a, _ = two_orgs
        user_a.set_password("supersecret123")
        user_a.account_status = "suspended"
        user_a.save()

        resp = APIClient().post(
            "/api/auth/login/",
            {"email": user_a.email, "password": "supersecret123"},
            format="json",
        )
        assert resp.status_code == 401

    def test_active_account_can_still_log_in(self, two_orgs):
        _, _, user_a, _ = two_orgs
        user_a.set_password("supersecret123")
        user_a.save()

        resp = APIClient().post(
            "/api/auth/login/",
            {"email": user_a.email, "password": "supersecret123"},
            format="json",
        )
        assert resp.status_code == 200, resp.content

    def test_existing_session_is_revoked_when_account_is_locked(self, two_orgs):
        _, _, user_a, _ = two_orgs
        user_a.set_password("supersecret123")
        user_a.save()
        client = APIClient()
        assert client.post(
            "/api/auth/login/",
            {"email": user_a.email, "password": "supersecret123"},
            format="json",
        ).status_code == 200
        assert client.get("/api/auth/me/").status_code == 200

        user_a.account_status = "locked"
        user_a.save(update_fields=["account_status"])

        # The session predates the status change, so only a per-request check
        # can revoke it.
        assert client.get("/api/auth/me/").status_code == 403


@pytest.mark.django_db
class TestSec13EmailLogScoping:
    def test_staff_cannot_read_another_orgs_email_log(self, two_orgs):
        from apps.notifications.models import EmailLog

        org_a, _, user_a, user_b = two_orgs
        user_a.is_staff = True
        user_a.save(update_fields=["is_staff"])
        EmailLog.objects.create(
            user=user_b, recipient_email=user_b.email, subject="Org B secret", status="sent",
        )
        mine = EmailLog.objects.create(
            user=user_a, recipient_email=user_a.email, subject="Org A mail", status="sent",
        )

        resp = client_for(user_a).get("/api/email-logs/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        assert {str(row["id"]) for row in rows} == {str(mine.id)}
