"""Mandatory tests 1, 2, 8, 9: cross-organization isolation, membership required, IDOR, multi-org users."""
from rest_framework.test import APIClient

from tests.base import CoreTestCase


def usernames(response):
    return {m["user"]["username"] for m in response.data["results"]}


class OrganizationIsolationTests(CoreTestCase):
    def test_1_user_of_a_cannot_see_data_of_b(self):
        client = self.api(self.manager_a, self.org_a)
        members = client.get("/api/v1/members/")
        self.assertEqual(members.status_code, 200)
        self.assertEqual(usernames(members), {"manager-a", "manager-a2", "employee-a", "multi"})
        self.assertNotIn("manager-b", usernames(members))

        # Asking for B's context with A credentials is refused outright.
        spoofed = self.api(self.manager_a, self.org_b).get("/api/v1/members/")
        self.assertEqual(spoofed.status_code, 403)
        self.assertEqual(spoofed.data["code"], "no_organization_access")

    def test_roles_audit_and_catalog_are_scoped_to_the_selected_organization(self):
        roles = self.api(self.manager_a, self.org_a).get("/api/v1/roles/")
        self.assertEqual(roles.status_code, 200)
        a_role_ids = {r["id"] for r in roles.data}
        b_role_ids = {r.pk for r in self.org_b.roles.all()}
        self.assertFalse(a_role_ids & b_role_ids)

        audit = self.api(self.manager_a, self.org_a).get("/api/v1/audit-logs/")
        self.assertEqual(audit.status_code, 200)
        from apps.audit.models import AuditLog

        self.assertEqual(audit.data["count"], AuditLog.objects.filter(organization=self.org_a).count())
        self.assertGreater(AuditLog.objects.filter(organization=self.org_b).count(), 0)
        self.assertEqual(
            self.api(self.manager_a, self.org_b).get("/api/v1/audit-logs/").status_code, 403
        )

    def test_2_user_without_membership_cannot_use_organization_apis(self):
        stranger = self.api(self.stranger, self.org_a)
        for path in ("/api/v1/members/", "/api/v1/roles/", "/api/v1/audit-logs/", "/api/v1/permissions/", "/probe/"):
            response = stranger.get(path)
            self.assertEqual(response.status_code, 403, path)
            self.assertEqual(response.data["code"], "no_organization_access", path)
        self.assertEqual(self.api().get("/api/v1/members/").status_code, 401)

    def test_unknown_and_forbidden_organizations_are_indistinguishable(self):
        client = APIClient()
        client.force_authenticate(self.stranger)
        client.credentials(HTTP_X_ORGANIZATION="no-such-org")
        unknown = client.get("/api/v1/members/")
        client.credentials(HTTP_X_ORGANIZATION=self.org_a.slug)
        forbidden = client.get("/api/v1/members/")
        self.assertEqual((unknown.status_code, unknown.data), (forbidden.status_code, forbidden.data))

    def test_missing_organization_header_is_rejected(self):
        response = self.api(self.manager_a).get("/api/v1/members/")
        self.assertEqual(response.status_code, 403)
        self.assertEqual(response.data["code"], "organization_required")

    def test_suspended_membership_loses_all_access(self):
        membership = self.membership(self.employee_a, self.org_a)
        membership.status = "suspended"
        membership.save()
        self.assertEqual(self.api(self.employee_a, self.org_a).get("/probe/").status_code, 403)

    def test_inactive_organization_blocks_its_members(self):
        self.org_a.is_active = False
        self.org_a.save()
        self.assertEqual(self.api(self.manager_a, self.org_a).get("/api/v1/members/").status_code, 403)


class IdorTests(CoreTestCase):
    """Mandatory test 8: changing an URL/record id must not bypass authorization."""

    def setUp(self):
        super().setUp()
        self.b_member = self.membership(self.employee_b, self.org_b)
        self.b_role = self.role(self.org_b, "employee")
        self.client_a = self.api(self.manager_a, self.org_a)

    def test_foreign_member_ids_are_invisible(self):
        self.assertEqual(self.client_a.get(f"/api/v1/members/{self.b_member.pk}/").status_code, 404)
        self.assertEqual(self.client_a.patch(f"/api/v1/members/{self.b_member.pk}/", {"status": "left"}, format="json").status_code, 404)
        self.assertEqual(
            self.client_a.put(f"/api/v1/members/{self.b_member.pk}/roles/", {"role_ids": []}, format="json").status_code, 404
        )
        self.assertEqual(self.client_a.get(f"/api/v1/members/{self.b_member.pk}/overrides/").status_code, 404)
        self.b_member.refresh_from_db()
        self.assertEqual(self.b_member.status, "active")

    def test_foreign_role_ids_are_invisible(self):
        self.assertEqual(self.client_a.get(f"/api/v1/roles/{self.b_role.pk}/").status_code, 404)
        self.assertEqual(self.client_a.delete(f"/api/v1/roles/{self.b_role.pk}/").status_code, 404)
        self.assertEqual(
            self.client_a.put(f"/api/v1/roles/{self.b_role.pk}/permissions/", {"grants": []}, format="json").status_code, 404
        )
        self.assertTrue(self.b_role.grants.exists())

    def test_assigning_a_foreign_role_to_own_member_is_rejected(self):
        response = self.client_a.put(
            f"/api/v1/members/{self.membership(self.employee_a, self.org_a).pk}/roles/",
            {"role_ids": [self.b_role.pk]}, format="json",
        )
        self.assertEqual(response.status_code, 404)

    def test_path_organization_id_is_validated_not_trusted(self):
        self.assertEqual(self.client_a.get(f"/api/v1/organizations/{self.org_b.pk}/modules/").status_code, 403)
        self.assertEqual(self.client_a.get(f"/api/v1/organizations/{self.org_a.pk}/modules/").status_code, 200)
        # numeric id in the header works the same as the slug - and is validated the same way
        numeric = APIClient()
        numeric.force_authenticate(self.manager_a)
        numeric.credentials(HTTP_X_ORGANIZATION=str(self.org_b.pk))
        self.assertEqual(numeric.get("/api/v1/members/").status_code, 403)

    def test_customer_cannot_use_platform_endpoints(self):
        self.assertEqual(self.client_a.get("/api/v1/organizations/").status_code, 403)
        self.assertEqual(
            self.client_a.post("/api/v1/organizations/", {"name": "x", "slug": "x"}, format="json").status_code, 403
        )
        self.assertEqual(
            self.client_a.post(f"/api/v1/organizations/{self.org_a.pk}/modules/attendance/disable/").status_code, 403
        )


class MultiOrganizationUserTests(CoreTestCase):
    """Mandatory test 9: one login, several organizations - each context shows only its own data."""

    def test_each_selected_organization_exposes_only_its_own_scope(self):
        in_a = self.api(self.multi, self.org_a).get("/api/v1/members/")
        self.assertEqual(in_a.status_code, 200)
        self.assertNotIn("manager-b", usernames(in_a))

        # In B the same person is only an employee: no member management at all.
        in_b = self.api(self.multi, self.org_b).get("/api/v1/members/")
        self.assertEqual(in_b.status_code, 403)

    def test_me_reflects_the_selected_context(self):
        me_a = self.api(self.multi, self.org_a).get("/api/v1/me/").data
        me_b = self.api(self.multi, self.org_b).get("/api/v1/me/").data
        self.assertEqual(me_a["context"]["organization"]["slug"], "org-a")
        self.assertEqual(me_b["context"]["organization"]["slug"], "org-b")
        self.assertIn("members.view", me_a["context"]["permissions"])
        self.assertNotIn("members.view", me_b["context"]["permissions"])
        self.assertEqual({o["slug"] for o in me_a["organizations"]}, {"org-a", "org-b"})
        self.assertEqual(len(me_a["memberships"]), 2)

    def test_module_state_is_per_organization(self):
        from apps.modules import services as modules

        modules.disable_module(actor=self.root, organization=self.org_b, module_code="attendance")
        self.assertEqual(self.api(self.multi, self.org_a).get("/probe/").status_code, 200)
        blocked = self.api(self.multi, self.org_b).get("/probe/")
        self.assertEqual(blocked.status_code, 403)
        self.assertEqual(blocked.data["code"], "module_disabled")
