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

from apps.companies.models import Company, Organization, OrganizationMembership

User = get_user_model()


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


@pytest.fixture
def two_orgs(db):
    from apps.hr.signals import ensure_employee_stub
    org_a = Organization.objects.create(name="Org A")
    org_b = Organization.objects.create(name="Org B")
    user_a = User.objects.create_user(email="a@tenancy.test", password="p")
    user_b = User.objects.create_user(email="b@tenancy.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 TestCompanyTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        org_a, org_b, user_a, user_b = two_orgs
        Company.objects.create(organization=org_a, name="Company A")
        Company.objects.create(organization=org_b, name="Company B")

        resp = client_for(user_a).get("/api/companies/")
        assert resp.status_code == 200
        names = {row["name"] for row in resp.data["results"]} if "results" in resp.data else {row["name"] for row in resp.data}
        assert names == {"Company A"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        org_a, org_b, user_a, user_b = two_orgs
        other_company = Company.objects.create(organization=org_b, name="Company B")

        resp = client_for(user_a).get(f"/api/companies/{other_company.id}/")
        assert resp.status_code == 404

    def test_create_tags_caller_active_org(self, two_orgs):
        org_a, org_b, user_a, user_b = two_orgs
        # Superuser bypasses RBAC's HasMenuPermission gate; reassign its
        # auto-created org membership to org_a so the request resolves there.
        admin_a = User.objects.create_superuser(email="admin-a@tenancy.test", password="p")
        auto_org = admin_a.organizations.first()
        OrganizationMembership.objects.filter(organization=auto_org, user=admin_a).update(organization=org_a)
        auto_org.delete()

        resp = client_for(admin_a).post("/api/companies/", {"name": "New Co"}, format="json")
        assert resp.status_code == 201, resp.content
        created = Company.objects.get(pk=resp.data["id"])
        assert str(created.organization_id).replace("-", "") == str(org_a.id).replace("-", "")


@pytest.mark.django_db
class TestEmployeeTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from apps.hr.models import Employee
        org_a, org_b, user_a, user_b = two_orgs
        emp_a = Employee.objects.get(user=user_a)
        emp_a.employee_id = "EMP-A"
        emp_a.save()
        emp_b = Employee.objects.get(user=user_b)
        emp_b.employee_id = "EMP-B"
        emp_b.save()

        resp = client_for(user_a).get("/api/employees/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        ids = {r["employee_id"] for r in rows}
        assert ids == {"EMP-A"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.hr.models import Employee
        org_a, org_b, user_a, user_b = two_orgs
        emp_b = Employee.objects.get(user=user_b)

        resp = client_for(user_a).get(f"/api/employees/{emp_b.id}/")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestProjectTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from apps.projects.models import Project
        org_a, org_b, user_a, user_b = two_orgs
        Project.objects.create(organization=org_a, name="Project A")
        Project.objects.create(organization=org_b, name="Project B")

        # user_a needs the project RBAC domain to list; grant it directly.
        from apps.core.models import Permission, Role, UserRole
        role = Role.objects.create(name="Project Tester", slug="project-tester-tenancy")
        perm, _ = Permission.objects.get_or_create(domain="project", action="*", defaults={"label": "All project"})
        role.permissions.add(perm)
        UserRole.objects.create(user=user_a, role=role, organization=org_a)

        resp = client_for(user_a).get("/api/projects/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        names = {r["name"] for r in rows}
        assert names == {"Project A"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.projects.models import Project
        from apps.core.models import Permission, Role, UserRole
        org_a, org_b, user_a, user_b = two_orgs
        other_project = Project.objects.create(organization=org_b, name="Project B")

        role = Role.objects.create(name="Project Tester 2", slug="project-tester-tenancy-2")
        perm, _ = Permission.objects.get_or_create(domain="project", action="*", defaults={"label": "All project"})
        role.permissions.add(perm)
        UserRole.objects.create(user=user_a, role=role, organization=org_a)

        resp = client_for(user_a).get(f"/api/projects/{other_project.id}/")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestEventTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from datetime import datetime, timedelta
        from apps.events.models import Event
        org_a, org_b, user_a, user_b = two_orgs
        Event.objects.create(
            organization=org_a, name="Event A", event_type="conference", organizer=user_a,
            start_date=datetime.now(), end_date=datetime.now() + timedelta(hours=2),
        )
        Event.objects.create(
            organization=org_b, name="Event B", event_type="conference", organizer=user_b,
            start_date=datetime.now(), end_date=datetime.now() + timedelta(hours=2),
        )

        from apps.core.models import Permission, Role, UserRole
        role = Role.objects.create(name="Event Tester", slug="event-tester-tenancy")
        perm, _ = Permission.objects.get_or_create(domain="events", action="*", defaults={"label": "All events"})
        role.permissions.add(perm)
        UserRole.objects.create(user=user_a, role=role, organization=org_a)

        resp = client_for(user_a).get("/api/events/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        names = {r["name"] for r in rows}
        assert names == {"Event A"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from datetime import datetime, timedelta
        from apps.events.models import Event
        from apps.core.models import Permission, Role, UserRole
        org_a, org_b, user_a, user_b = two_orgs
        other_event = Event.objects.create(
            organization=org_b, name="Event B", event_type="conference", organizer=user_b,
            start_date=datetime.now(), end_date=datetime.now() + timedelta(hours=2),
        )

        role = Role.objects.create(name="Event Tester 2", slug="event-tester-tenancy-2")
        perm, _ = Permission.objects.get_or_create(domain="events", action="*", defaults={"label": "All events"})
        role.permissions.add(perm)
        UserRole.objects.create(user=user_a, role=role, organization=org_a)

        resp = client_for(user_a).get(f"/api/events/{other_event.id}/")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestTaskTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from apps.tasks.models import Task
        org_a, org_b, user_a, user_b = two_orgs
        Task.objects.create(organization=org_a, title="Task A", created_by=user_a)
        Task.objects.create(organization=org_b, title="Task B", created_by=user_b)

        from apps.core.models import Permission, Role, UserRole
        role = Role.objects.create(name="Task Tester Tenancy", slug="task-tester-tenancy")
        perm, _ = Permission.objects.get_or_create(domain="tasks", action="*", defaults={"label": "All tasks"})
        role.permissions.add(perm)
        UserRole.objects.create(user=user_a, role=role, organization=org_a)

        resp = client_for(user_a).get("/api/tasks/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        titles = {r["title"] for r in rows}
        assert titles == {"Task A"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.tasks.models import Task
        from apps.core.models import Permission, Role, UserRole
        org_a, org_b, user_a, user_b = two_orgs
        other_task = Task.objects.create(organization=org_b, title="Task B", created_by=user_b)

        role = Role.objects.create(name="Task Tester Tenancy 2", slug="task-tester-tenancy-2")
        perm, _ = Permission.objects.get_or_create(domain="tasks", action="*", defaults={"label": "All tasks"})
        role.permissions.add(perm)
        UserRole.objects.create(user=user_a, role=role, organization=org_a)

        resp = client_for(user_a).get(f"/api/tasks/{other_task.id}/")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestAccountTenantScoping:
    @staticmethod
    def _grant_accounting(user, organization, slug):
        from apps.core.models import Permission, Role, UserRole
        role = Role.objects.create(name=f"Accounting Tester {slug}", slug=slug)
        perm, _ = Permission.objects.get_or_create(domain="accounting", action="*", defaults={"label": "All accounting"})
        role.permissions.add(perm)
        UserRole.objects.create(user=user, role=role, organization=organization)

    def test_list_only_shows_own_org(self, two_orgs):
        from apps.accounting.models import Account
        org_a, org_b, user_a, user_b = two_orgs
        Account.objects.create(organization=org_a, code="1000", name="Cash A", type="Asset", normal_balance="Debit")
        Account.objects.create(organization=org_b, code="1000", name="Cash B", type="Asset", normal_balance="Debit")
        self._grant_accounting(user_a, org_a, "accounting-tester-tenancy")

        resp = client_for(user_a).get("/api/accounting/accounts/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        names = {r["name"] for r in rows}
        assert names == {"Cash A"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.accounting.models import Account
        org_a, org_b, user_a, user_b = two_orgs
        other_account = Account.objects.create(organization=org_b, code="1000", name="Cash B", type="Asset", normal_balance="Debit")
        self._grant_accounting(user_a, org_a, "accounting-tester-tenancy-2")

        resp = client_for(user_a).get(f"/api/accounting/accounts/{other_account.id}/")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestVendorTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from apps.asset_management.models import Vendor
        org_a, org_b, user_a, user_b = two_orgs
        Vendor.objects.create(organization=org_a, name="Vendor A")
        Vendor.objects.create(organization=org_b, name="Vendor B")

        resp = client_for(user_a).get("/api/vendors/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        names = {r["name"] for r in rows}
        assert names == {"Vendor A"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.asset_management.models import Vendor
        org_a, org_b, user_a, user_b = two_orgs
        other_vendor = Vendor.objects.create(organization=org_b, name="Vendor B")

        resp = client_for(user_a).get(f"/api/vendors/{other_vendor.id}/")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestAssetTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from apps.asset_management.models import Asset
        org_a, org_b, user_a, user_b = two_orgs
        Asset.objects.create(organization=org_a, name="Laptop A", asset_tag="TAG-A")
        Asset.objects.create(organization=org_b, name="Laptop B", asset_tag="TAG-B")

        resp = client_for(user_a).get("/api/assets/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        names = {r["name"] for r in rows}
        assert names == {"Laptop A"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.asset_management.models import Asset
        org_a, org_b, user_a, user_b = two_orgs
        other_asset = Asset.objects.create(organization=org_b, name="Laptop B", asset_tag="TAG-B")

        resp = client_for(user_a).get(f"/api/assets/{other_asset.id}/")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestRequisitionTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from apps.procurements.models import Requisition
        from apps.core.models import Permission, Role, UserRole
        org_a, org_b, user_a, user_b = two_orgs
        Requisition.objects.create(organization=org_a, title="Req A", requested_by=user_a)
        Requisition.objects.create(organization=org_b, title="Req B", requested_by=user_b)

        role = Role.objects.create(name="Procurement Tester", slug="procurement-tester-tenancy")
        perm, _ = Permission.objects.get_or_create(domain="procurements", action="*", defaults={"label": "All procurements"})
        role.permissions.add(perm)
        UserRole.objects.create(user=user_a, role=role, organization=org_a)

        resp = client_for(user_a).get("/api/requisitions/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        titles = {r["title"] for r in rows}
        assert titles == {"Req A"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.procurements.models import Requisition
        from apps.core.models import Permission, Role, UserRole
        org_a, org_b, user_a, user_b = two_orgs
        other_req = Requisition.objects.create(organization=org_b, title="Req B", requested_by=user_b)

        role = Role.objects.create(name="Procurement Tester 2", slug="procurement-tester-tenancy-2")
        perm, _ = Permission.objects.get_or_create(domain="procurements", action="*", defaults={"label": "All procurements"})
        role.permissions.add(perm)
        UserRole.objects.create(user=user_a, role=role, organization=org_a)

        resp = client_for(user_a).get(f"/api/requisitions/{other_req.id}/")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestCashAdvancePostsToCallerOwnOrgAccount:
    """Regression test for the Account-lookup correctness bug: two orgs each
    with their own '1200'/'1000' accounts, issuing a CashAdvance in org A must
    post to org A's own accounts, never org B's, even though both share the
    same account code (Account.code is per-org unique, not global)."""

    def test_issue_posts_to_own_org_accounts_not_other_org(self, two_orgs):
        from apps.accounting.models import Account, JournalLine
        from apps.companies.models import OrganizationMembership
        from apps.hr.models import Employee
        from apps.projects.models import Project, ProjectFund

        org_a, org_b, user_a, user_b = two_orgs

        # Both orgs have their own accounts sharing the same business codes.
        advance_a = Account.objects.create(organization=org_a, code="1200", name="Advance A", type="Asset", normal_balance="Debit")
        cash_a = Account.objects.create(organization=org_a, code="1000", name="Cash A", type="Asset", normal_balance="Debit", is_cash=True)
        Account.objects.create(organization=org_b, code="1200", name="Advance B", type="Asset", normal_balance="Debit")
        Account.objects.create(organization=org_b, code="1000", name="Cash B", type="Asset", normal_balance="Debit", is_cash=True)

        project = Project.objects.create(organization=org_a, name="Org A Project")
        fund = ProjectFund.objects.create(organization=org_a, project=project, source="Donor A", amount=1_000_000, currency="IDR", status="received")

        # Superuser rewired into org_a, matching the established test pattern.
        admin = User.objects.create_superuser(email="cashadv-admin@tenancy.test", password="p")
        auto_org = admin.organizations.first()
        OrganizationMembership.objects.filter(organization=auto_org, user=admin).update(organization=org_a)
        auto_org.delete()

        recipient = Employee.objects.get(user=user_a)
        resp = client_for(admin).post("/api/cash-advances/", {
            "project": str(project.id), "fund": str(fund.id), "recipient": str(recipient.id),
            "purpose": "Tenancy regression", "amount": "100000", "currency": "IDR", "date_issued": "2026-07-01",
        }, format="multipart")
        assert resp.status_code == 201, resp.content

        lines = JournalLine.objects.filter(journal__source_project_fund=fund)
        accounts_hit = {str(a).replace("-", "") for a in lines.values_list("account_id", flat=True)}
        expected = {str(advance_a.id).replace("-", ""), str(cash_a.id).replace("-", "")}
        assert accounts_hit == expected


@pytest.mark.django_db
class TestRbacPerOrgMembership:
    """A user's role in one org must not leak permission into another org
    they also belong to — RBAC is scoped per org-membership, not per user."""

    def _grant(self, user, organization, slug, *codenames):
        from apps.core.models import Permission, Role, UserRole
        role = Role.objects.create(name=f"Role {slug}", slug=slug)
        for codename in codenames:
            domain, action = codename.split(".", 1)
            perm, _ = Permission.objects.get_or_create(domain=domain, action=action, defaults={"label": codename})
            role.permissions.add(perm)
        UserRole.objects.create(user=user, role=role, organization=organization)
        return role

    def test_role_in_one_org_does_not_grant_permission_in_another(self, two_orgs):
        from apps.core.tenancy import get_active_organization
        from apps.core.permissions import user_has_full_scope

        org_a, org_b, user_a, user_b = two_orgs
        # user_a also joins org_b, but holds a full-scope role in org_a only.
        OrganizationMembership.objects.create(organization=org_b, user=user_a)
        self._grant(user_a, org_a, "full-scope-tasks-org-a", "tasks.delete")

        class _Req:
            def __init__(self, session):
                self.user = user_a
                self.session = session

        active_a = get_active_organization(_Req({"active_org_id": str(org_a.id)}))
        active_b = get_active_organization(_Req({"active_org_id": str(org_b.id)}))
        assert user_has_full_scope(user_a, "tasks", active_a) is True
        assert user_has_full_scope(user_a, "tasks", active_b) is False

    def test_me_endpoint_returns_roles_for_active_org_only(self, two_orgs):
        org_a, org_b, user_a, user_b = two_orgs
        OrganizationMembership.objects.create(organization=org_b, user=user_a)
        self._grant(user_a, org_a, "me-endpoint-role-org-a", "tasks.view")

        client = client_for(user_a)
        session = client.session
        session["active_org_id"] = str(org_a.id)
        session.save()
        resp_a = client.get("/api/auth/me/")
        assert resp_a.status_code == 200
        assert {r["slug"] for r in resp_a.data["roles"]} == {"me-endpoint-role-org-a"}

        session = client.session
        session["active_org_id"] = str(org_b.id)
        session.save()
        resp_b = client.get("/api/auth/me/")
        assert resp_b.status_code == 200
        assert resp_b.data["roles"] == []

    def test_admin_users_endpoint_scoped_to_active_org(self, two_orgs):
        org_a, org_b, user_a, user_b = two_orgs
        admin_a = User.objects.create_superuser(email="rbac-admin-a@tenancy.test", password="p")
        auto_org = admin_a.organizations.first()
        OrganizationMembership.objects.filter(organization=auto_org, user=admin_a).update(organization=org_a)
        auto_org.delete()

        client = client_for(admin_a)
        resp = client.get("/api/rbac/users/")
        assert resp.status_code == 200
        emails = {row["email"] for row in resp.data["results"]} if "results" in resp.data else {row["email"] for row in resp.data}
        assert "a@tenancy.test" in emails
        assert "b@tenancy.test" not in emails

    def test_assign_roles_to_user_outside_active_org_404s(self, two_orgs):
        org_a, org_b, user_a, user_b = two_orgs
        admin_a = User.objects.create_superuser(email="rbac-admin-a2@tenancy.test", password="p")
        auto_org = admin_a.organizations.first()
        OrganizationMembership.objects.filter(organization=auto_org, user=admin_a).update(organization=org_a)
        auto_org.delete()

        client = client_for(admin_a)
        resp = client.post(f"/api/rbac/users/{user_b.pk}/roles/", {"role_ids": []}, format="json")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestHelpdeskTicketTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from apps.administrations.models import HelpdeskTicket
        org_a, org_b, user_a, user_b = two_orgs
        HelpdeskTicket.objects.create(organization=org_a, requester=user_a, subject="A issue", description="d")
        HelpdeskTicket.objects.create(organization=org_b, requester=user_b, subject="B issue", description="d")

        resp = client_for(user_a).get("/api/helpdesk-tickets/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        subjects = {r["subject"] for r in rows}
        assert subjects == {"A issue"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.administrations.models import HelpdeskTicket
        org_a, org_b, user_a, user_b = two_orgs
        ticket_b = HelpdeskTicket.objects.create(organization=org_b, requester=user_b, subject="B issue", description="d")

        resp = client_for(user_a).get(f"/api/helpdesk-tickets/{ticket_b.id}/")
        assert resp.status_code == 404

    def test_ticket_number_sequence_scoped_per_org(self, two_orgs):
        """Regression: HelpdeskTicket.save() must scan for the max ticket_number
        within the caller's own org, not globally — otherwise org B's first
        ticket collides with / skips past org A's sequence."""
        from apps.administrations.models import HelpdeskTicket
        org_a, org_b, user_a, user_b = two_orgs
        for _ in range(3):
            HelpdeskTicket.objects.create(organization=org_a, requester=user_a, subject="A", description="d")
        ticket_b = HelpdeskTicket.objects.create(organization=org_b, requester=user_b, subject="B", description="d")
        assert ticket_b.ticket_number == "HD-00001"


@pytest.mark.django_db
class TestActivityTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from apps.activities.models import Activity
        org_a, org_b, user_a, user_b = two_orgs
        Activity.objects.create(organization=org_a, title="A activity", activity_type="call", user=user_a)
        Activity.objects.create(organization=org_b, title="B activity", activity_type="call", user=user_b)

        resp = client_for(user_a).get("/api/activities/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        titles = {r["title"] for r in rows}
        assert titles == {"A activity"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.activities.models import Activity
        org_a, org_b, user_a, user_b = two_orgs
        activity_b = Activity.objects.create(organization=org_b, title="B activity", activity_type="call", user=user_b)

        resp = client_for(user_a).get(f"/api/activities/{activity_b.id}/")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestPublicationTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from apps.publications.models import Publication
        org_a, org_b, user_a, user_b = two_orgs
        Publication.objects.create(organization=org_a, title="A pub", content="c", author=user_a)
        Publication.objects.create(organization=org_b, title="B pub", content="c", author=user_b)

        resp = client_for(user_a).get("/api/publications/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        titles = {r["title"] for r in rows}
        assert titles == {"A pub"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.publications.models import Publication
        org_a, org_b, user_a, user_b = two_orgs
        pub_b = Publication.objects.create(organization=org_b, title="B pub", content="c", author=user_b)

        resp = client_for(user_a).get(f"/api/publications/{pub_b.id}/")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestAnnouncementBroadcastScopedToOrg:
    def test_announcement_notifies_only_own_org_active_employees(self, two_orgs):
        """Regression: AnnouncementViewSet._notify_employees must filter
        Employee by the announcement's organization — otherwise creating an
        announcement in org A notifies every active employee platform-wide."""
        from apps.administrations.models import Announcement
        from apps.hr.models import Employee
        from apps.notifications.models import Notification

        org_a, org_b, user_a, user_b = two_orgs
        Employee.objects.filter(user=user_a).update(status="active")
        Employee.objects.filter(user=user_b).update(status="active")

        from django.utils import timezone
        announcement = Announcement.objects.create(
            organization=org_a, title="A-only news", content="c", author=user_a,
            valid_from=timezone.now(),
        )
        from apps.administrations.views import AnnouncementViewSet
        AnnouncementViewSet()._notify_employees(announcement)

        assert not Notification.objects.filter(user=user_b, notification_type="administrations").exists()


@pytest.mark.django_db
class TestSidebarMyRequestsScopedToOrg:
    def test_car_booking_my_requests_scoped_to_org(self, two_orgs):
        """Regression: sidebar 'my requests' counts must filter by the
        caller's active org, not just requester= — otherwise a user with
        memberships in multiple orgs would see counts bleeding across orgs."""
        from apps.administrations.models import CarBooking
        from datetime import datetime, timedelta

        org_a, org_b, user_a, user_b = two_orgs
        CarBooking.objects.create(
            organization=org_a, requester=user_a, vehicle_name="V", purpose="P",
            start_datetime=datetime.now(), end_datetime=datetime.now() + timedelta(hours=1),
            destination="D", status="pending",
        )
        # Same requester id in org B's queryset shouldn't be reachable via org A's request.
        CarBooking.objects.create(
            organization=org_b, requester=user_b, vehicle_name="V", purpose="P",
            start_datetime=datetime.now(), end_datetime=datetime.now() + timedelta(hours=1),
            destination="D", status="pending",
        )

        resp = client_for(user_a).get("/api/sidebar/badges/")
        assert resp.status_code == 200
        assert resp.data["car_booking_my_requests"] == 1


@pytest.mark.django_db
class TestConversationTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from apps.chat.models import Conversation, ConversationParticipant

        org_a, org_b, user_a, user_b = two_orgs
        conv_a = Conversation.objects.create(organization=org_a, created_by=user_a, name="A convo")
        ConversationParticipant.objects.create(conversation=conv_a, user=user_a)
        conv_b = Conversation.objects.create(organization=org_b, created_by=user_b, name="B convo")
        ConversationParticipant.objects.create(conversation=conv_b, user=user_b)

        resp = client_for(user_a).get("/api/chat/conversations/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        names = {r["name"] for r in rows}
        assert names == {"A convo"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.chat.models import Conversation, ConversationParticipant

        org_a, org_b, user_a, user_b = two_orgs
        conv_b = Conversation.objects.create(organization=org_b, created_by=user_b, name="B convo")
        ConversationParticipant.objects.create(conversation=conv_b, user=user_b)

        resp = client_for(user_a).get(f"/api/chat/conversations/{conv_b.id}/")
        assert resp.status_code == 404

    def test_direct_rejects_cross_org_user_id(self, two_orgs):
        """Regression: ConversationViewSet.direct created a DM with any user_id
        platform-wide, no org check — confirm it now 404s cross-org."""
        org_a, org_b, user_a, user_b = two_orgs

        resp = client_for(user_a).post("/api/chat/conversations/direct/", {"user_id": str(user_b.id)}, format="json")
        assert resp.status_code == 404

    def test_direct_succeeds_for_same_org_user(self, two_orgs):
        from apps.companies.models import OrganizationMembership

        org_a, org_b, user_a, user_b = two_orgs
        user_a2 = User.objects.create_user(email="a2@tenancy.test", password="p")
        OrganizationMembership.objects.create(organization=org_a, user=user_a2)

        resp = client_for(user_a).post("/api/chat/conversations/direct/", {"user_id": str(user_a2.id)}, format="json")
        assert resp.status_code == 200
        from apps.chat.models import Conversation
        created = Conversation.objects.get(pk=resp.data["id"])
        assert str(created.organization_id).replace("-", "") == str(org_a.id).replace("-", "")


@pytest.mark.django_db
class TestUserKeyTenantScoping:
    def test_list_excludes_other_org_keys(self, two_orgs):
        """Regression: UserKeyViewSet.list bulk-fetched public keys for any
        user_ids with no membership check — confirm cross-org ids are dropped."""
        from apps.chat.models import UserKey

        org_a, org_b, user_a, user_b = two_orgs
        UserKey.objects.update_or_create(user=user_a, defaults={"public_key": "a" * 44})
        UserKey.objects.update_or_create(user=user_b, defaults={"public_key": "b" * 44})

        resp = client_for(user_a).get(f"/api/chat/keys/?user_ids={user_a.id},{user_b.id}")
        assert resp.status_code == 200
        returned_user_ids = {str(row["user"]).replace("-", "") for row in resp.data}
        assert str(user_a.id).replace("-", "") in returned_user_ids
        assert str(user_b.id).replace("-", "") not in returned_user_ids


@pytest.mark.django_db
class TestURLTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from apps.tools.models import URL

        org_a, org_b, user_a, user_b = two_orgs
        URL.objects.create(organization=org_a, original_url="https://example.com/a", created_by=user_a)
        URL.objects.create(organization=org_b, original_url="https://example.com/b", created_by=user_b)

        resp = client_for(user_a).get("/api/urls/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        urls = {r["original_url"] for r in rows}
        assert urls == {"https://example.com/a"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.tools.models import URL

        org_a, org_b, user_a, user_b = two_orgs
        url_b = URL.objects.create(organization=org_b, original_url="https://example.com/b", created_by=user_b)

        resp = client_for(user_a).get(f"/api/urls/{url_b.id}/")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestQRCodeTenantScoping:
    def test_download_rejects_cross_org_short_code(self, two_orgs):
        """Regression: QRCodeViewSet.download bypassed get_queryset() entirely
        via QRCode.objects.get(short_code=...) — any authenticated user could
        download/increment-download-count on any org's QR code."""
        from apps.tools.models import QRCode

        org_a, org_b, user_a, user_b = two_orgs
        qr_b = QRCode.objects.create(organization=org_b, content="hello", created_by=user_b)

        resp = client_for(user_a).get(f"/api/qr-codes/{qr_b.short_code}/download/")
        assert resp.status_code == 404

    def test_download_succeeds_for_own_qr_code(self, two_orgs):
        import sys
        import types
        from unittest.mock import MagicMock
        from apps.tools.models import QRCode

        org_a, org_b, user_a, user_b = two_orgs
        qr_a = QRCode.objects.create(organization=org_a, content="hello", created_by=user_a)

        fake_qrcode = types.ModuleType("qrcode")
        fake_image = MagicMock()
        fake_image.save = lambda buffer, format=None: buffer.write(b"fake-png-bytes")
        fake_qrcode.make = lambda content: fake_image
        sys.modules["qrcode"] = fake_qrcode
        try:
            resp = client_for(user_a).get(f"/api/qr-codes/{qr_a.short_code}/download/")
        finally:
            del sys.modules["qrcode"]
        assert resp.status_code == 200


@pytest.mark.django_db
class TestDisseminationPostTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from apps.disseminations.models import Post

        org_a, org_b, user_a, user_b = two_orgs
        Post.objects.create(organization=org_a, title="A post", author=user_a)
        Post.objects.create(organization=org_b, title="B post", author=user_b)

        resp = client_for(user_a).get("/api/disseminations/posts/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        titles = {r["title"] for r in rows}
        assert titles == {"A post"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.disseminations.models import Post

        org_a, org_b, user_a, user_b = two_orgs
        post_b = Post.objects.create(organization=org_b, title="B post", author=user_b)

        resp = client_for(user_a).get(f"/api/disseminations/posts/{post_b.id}/")
        assert resp.status_code == 404

    def test_ensure_tracking_link_stamps_post_organization(self, two_orgs):
        """Regression: ensure_tracking_link's URL.objects.create had no
        organization= — would violate the non-null constraint outright."""
        from apps.disseminations.models import Post
        from apps.disseminations.services import ensure_tracking_link

        org_a, org_b, user_a, user_b = two_orgs
        post = Post.objects.create(organization=org_a, title="A post", author=user_a, link_url="https://example.com/x")
        url = ensure_tracking_link(post)
        assert url.organization_id == org_a.id


@pytest.mark.django_db
class TestAnalyticsSiteTenantScoping:
    def test_list_only_shows_own_org(self, two_orgs):
        from apps.analytics.models import Site

        org_a, org_b, user_a, user_b = two_orgs
        Site.objects.create(organization=org_a, name="A Site", domain="a.example.com")
        Site.objects.create(organization=org_b, name="B Site", domain="b.example.com")

        resp = client_for(user_a).get("/api/analytics/sites/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        names = {r["name"] for r in rows}
        assert names == {"A Site"}

    def test_cross_org_detail_is_404_not_403(self, two_orgs):
        from apps.analytics.models import Site

        org_a, org_b, user_a, user_b = two_orgs
        site_b = Site.objects.create(organization=org_b, name="B Site", domain="b.example.com")

        resp = client_for(user_a).get(f"/api/analytics/sites/{site_b.id}/")
        assert resp.status_code == 404


@pytest.mark.django_db
class TestAccessLogWriteTimeOrgResolution:
    def test_shorten_url_resolves_to_owning_org(self, two_orgs, settings):
        """Regression: AccessLog had no organization at all, and its ViewSet
        was readable by any authenticated user platform-wide. Confirm the
        middleware resolves organization from the shortened URL's owner."""
        from apps.tools.models import URL
        from apps.analytics.models import AccessLog

        settings.BACKGROUND_TASKS_EAGER = True
        org_a, org_b, user_a, user_b = two_orgs
        url = URL.objects.create(organization=org_a, original_url="https://example.com/x", created_by=user_a)

        client_for(user_a).get(f"/api/urls/{url.short_code}/", HTTP_HOST="localhost")

        log = AccessLog.objects.filter(resource_type="shorten_url", resource_id=url.short_code).first()
        assert log is not None
        assert str(log.organization_id).replace("-", "") == str(org_a.id).replace("-", "")

    def test_accesslog_list_excludes_other_org(self, two_orgs, settings):
        from apps.analytics.models import AccessLog

        settings.BACKGROUND_TASKS_EAGER = True
        org_a, org_b, user_a, user_b = two_orgs
        AccessLog.objects.create(organization=org_a, resource_type="other", path="/a")
        AccessLog.objects.create(organization=org_b, resource_type="other", path="/b")

        resp = client_for(user_a).get("/api/analytics/access-logs/")
        assert resp.status_code == 200
        rows = resp.data["results"] if "results" in resp.data else resp.data
        paths = {r["path"] for r in rows}
        assert paths == {"/a"}


@pytest.mark.django_db
class TestReminderExpiringContractsAndLeaveScopedToOrg:
    def test_expiring_contracts_scoped_per_org(self, two_orgs):
        from datetime import timedelta
        from django.utils import timezone
        from apps.hr.models import Contract, Employee
        from apps.scheduler import reminders

        org_a, org_b, user_a, user_b = two_orgs
        emp_a = Employee.objects.get(user=user_a)
        emp_b = Employee.objects.get(user=user_b)
        target = timezone.localdate() + timedelta(days=30)
        Contract.objects.create(employee=emp_a, contract_type="full_time", status="active",
                                 start_date=target - timedelta(days=300), end_date=target)
        Contract.objects.create(employee=emp_b, contract_type="full_time", status="active",
                                 start_date=target - timedelta(days=300), end_date=target)

        assert reminders.remind_expiring_contracts() == 2
        assert reminders.remind_expiring_contracts() == 0  # dedup

    def test_approaching_leave_scoped_per_org(self, two_orgs):
        from datetime import timedelta
        from django.utils import timezone
        from apps.hr.models import Employee, LeaveRequest
        from apps.scheduler import reminders

        org_a, org_b, user_a, user_b = two_orgs
        emp_a = Employee.objects.get(user=user_a)
        emp_b = Employee.objects.get(user=user_b)
        tomorrow = timezone.localdate() + timedelta(days=1)
        LeaveRequest.objects.create(employee=emp_a, leave_type="annual", status="approved",
                                     start_date=tomorrow, end_date=tomorrow + timedelta(days=2))
        LeaveRequest.objects.create(employee=emp_b, leave_type="annual", status="approved",
                                     start_date=tomorrow, end_date=tomorrow + timedelta(days=2))

        assert reminders.remind_approaching_leave() == 2
        assert reminders.remind_approaching_leave() == 0  # dedup


@pytest.mark.django_db
class TestOrgProfileCurrentScopedToCaller:
    """Regression: OrgProfileViewSet._get_singleton() did Organization.objects.first()
    — every org's members saw the globally-first org's (CSIS Indonesia's) branding
    on /org-profile/current/, regardless of their own membership."""

    def test_returns_callers_own_org_not_the_first_org(self, two_orgs):
        org_a, org_b, user_a, user_b = two_orgs
        org_a.name = "Org A branding"
        org_a.save(update_fields=["name"])
        org_b.name = "Org B branding"
        org_b.save(update_fields=["name"])

        resp = client_for(user_b).get("/api/org-profile/current/")
        assert resp.status_code == 200
        assert resp.data["name"] == "Org B branding"

    def test_anonymous_get_returns_empty_not_another_orgs_branding(self, two_orgs):
        """Regression: GET must stay public for pre-login branding, but an
        anonymous caller has no organization to resolve — it must get an
        empty response, not another tenant's private branding leaking through."""
        org_a, org_b, user_a, user_b = two_orgs
        resp = APIClient().get("/api/org-profile/current/")
        assert resp.status_code == 200
        assert resp.data == {}

    def test_anonymous_patch_still_requires_authentication(self):
        resp = APIClient().patch("/api/org-profile/current/", {"name": "Hacked"}, format="json")
        assert resp.status_code == 403


@pytest.mark.django_db
class TestSetupStatusScopedToCaller:
    """Regression: SetupStatusView did Organization.objects.first() — a brand
    new org's superuser always saw setup_complete=True because CSIS Indonesia's
    (the first org's) profile was already complete."""

    def test_new_org_with_incomplete_profile_reports_incomplete(self, two_orgs):
        org_a, org_b, user_a, user_b = two_orgs
        # org_b starts with the default name/no contact info -> incomplete.
        resp = client_for(user_b).get("/api/setup/status/")
        assert resp.status_code == 200
        assert resp.data["org_profile_complete"] is False
        assert resp.data["setup_complete"] is False

    def test_complete_org_profile_reports_complete_for_its_own_members(self, two_orgs):
        org_a, org_b, user_a, user_b = two_orgs
        org_a.name = "Org A"
        org_a.email = "hello@orga.test"
        org_a.save(update_fields=["name", "email"])

        resp = client_for(user_a).get("/api/setup/status/")
        assert resp.status_code == 200
        assert resp.data["org_profile_complete"] is True


class _FakeRequest:
    def __init__(self, user):
        self.user = user
        self.session = {}


@pytest.mark.django_db
class TestGetActiveOrganization:
    def test_single_membership_resolves_unambiguously(self, two_orgs):
        from apps.core.tenancy import get_active_organization

        org_a, org_b, user_a, user_b = two_orgs
        resolved = get_active_organization(_FakeRequest(user_a))
        assert str(resolved.pk).replace("-", "") == str(org_a.pk).replace("-", "")

    def test_no_membership_raises_permission_denied(self, db):
        from rest_framework.exceptions import PermissionDenied
        from apps.core.tenancy import get_active_organization

        orphan = User.objects.create_user(email="orphan@tenancy.test", password="p")
        with pytest.raises(PermissionDenied):
            get_active_organization(_FakeRequest(orphan))
