from django.test import TestCase, tag

from epdb.logic import GroupManager, PackageManager, UserManager
from epdb.models import (
    Compound,
    GroupPackagePermission,
    Permission,
    UserPackagePermission,
)


@tag("api", "end2end")
class APIPermissionTestBase(TestCase):
    """
    Base class for API permission tests.

    Sets up common test data:
    - user1: Owner of packages
    - user2: User with various permissions
    - user3: User with no permissions
    - reviewed_package: Public package (reviewed=True)
    - unreviewed_package_owned: Unreviewed package owned by user1
    - unreviewed_package_read: Unreviewed package with READ permission for user2
    - unreviewed_package_write: Unreviewed package with WRITE permission for user2
    - unreviewed_package_all: Unreviewed package with ALL permission for user2
    - unreviewed_package_no_access: Unreviewed package with no permissions for user2/user3
    - group_package: Unreviewed package accessible via group permission
    - test_group: Group containing user2
    """

    @classmethod
    def setUpTestData(cls):
        # Create users
        cls.user1 = UserManager.create_user(
            "permission-user1",
            "permission-user1@envipath.com",
            "SuperSafe",
            set_setting=False,
            add_to_group=False,
            is_active=True,
        )
        cls.user2 = UserManager.create_user(
            "permission-user2",
            "permission-user2@envipath.com",
            "SuperSafe",
            set_setting=False,
            add_to_group=False,
            is_active=True,
        )
        cls.user3 = UserManager.create_user(
            "permission-user3",
            "permission-user3@envipath.com",
            "SuperSafe",
            set_setting=False,
            add_to_group=False,
            is_active=True,
        )

        # Delete default packages to ensure clean test data
        for user in [cls.user1, cls.user2, cls.user3]:
            default_pkg = user.default_package
            user.default_package = None
            user.save()
            if default_pkg:
                default_pkg.delete()

        # Create reviewed package (public)
        cls.reviewed_package = PackageManager.create_package(
            cls.user1, "Reviewed Package", "Public package"
        )
        cls.reviewed_package.reviewed = True
        cls.reviewed_package.save()

        # Create unreviewed packages with various permissions
        cls.unreviewed_package_owned = PackageManager.create_package(
            cls.user1, "User1 Owned Package", "Owned by user1"
        )

        cls.unreviewed_package_read = PackageManager.create_package(
            cls.user1, "User2 Read Package", "User2 has READ permission"
        )
        UserPackagePermission.objects.create(
            user=cls.user2, package=cls.unreviewed_package_read, permission=Permission.READ[0]
        )

        cls.unreviewed_package_write = PackageManager.create_package(
            cls.user1, "User2 Write Package", "User2 has WRITE permission"
        )
        UserPackagePermission.objects.create(
            user=cls.user2, package=cls.unreviewed_package_write, permission=Permission.WRITE[0]
        )

        cls.unreviewed_package_all = PackageManager.create_package(
            cls.user1, "User2 All Package", "User2 has ALL permission"
        )
        UserPackagePermission.objects.create(
            user=cls.user2, package=cls.unreviewed_package_all, permission=Permission.ALL[0]
        )

        cls.unreviewed_package_no_access = PackageManager.create_package(
            cls.user1, "No Access Package", "No permissions for user2/user3"
        )

        # Create group and group package
        cls.test_group = GroupManager.create_group(
            cls.user1, "Test Group", "Group for permission testing"
        )
        cls.test_group.user_member.add(cls.user2)
        cls.test_group.save()

        cls.group_package = PackageManager.create_package(
            cls.user1, "Group Package", "Accessible via group permission"
        )
        GroupPackagePermission.objects.create(
            group=cls.test_group, package=cls.group_package, permission=Permission.READ[0]
        )

        # Create test compounds in each package
        cls.reviewed_compound = Compound.create(
            cls.reviewed_package, "C", name="Reviewed Compound", description="Test compound"
        )
        cls.owned_compound = Compound.create(
            cls.unreviewed_package_owned, "CC", name="Owned Compound", description="Test compound"
        )
        cls.read_compound = Compound.create(
            cls.unreviewed_package_read, "CCC", name="Read Compound", description="Test compound"
        )
        cls.write_compound = Compound.create(
            cls.unreviewed_package_write, "CCCC", name="Write Compound", description="Test compound"
        )
        cls.all_compound = Compound.create(
            cls.unreviewed_package_all, "CCCCC", name="All Compound", description="Test compound"
        )
        cls.no_access_compound = Compound.create(
            cls.unreviewed_package_no_access,
            "CCCCCC",
            name="No Access Compound",
            description="Test compound",
        )
        cls.group_compound = Compound.create(
            cls.group_package, "CCCCCCC", name="Group Compound", description="Test compound"
        )


@tag("api", "end2end")
class PackageListPermissionTest(APIPermissionTestBase):
    """
    Test permissions for /api/v1/packages/ endpoint.

    Special case: This endpoint allows anonymous access (auth=None)
    """

    ENDPOINT = "/api/v1/packages/"

    def test_anonymous_user_sees_only_reviewed_packages(self):
        """Anonymous users should only see reviewed packages."""
        self.client.logout()
        response = self.client.get(self.ENDPOINT)

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        # Should only see reviewed package
        self.assertEqual(payload["total_items"], 1)
        self.assertEqual(payload["items"][0]["uuid"], str(self.reviewed_package.uuid))
        self.assertEqual(payload["items"][0]["review_status"], "reviewed")

    def test_authenticated_user_sees_all_readable_packages(self):
        """Authenticated users see reviewed + packages they have access to."""
        self.client.force_login(self.user2)
        response = self.client.get(self.ENDPOINT)

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        # user2 should see:
        # - reviewed_package (public)
        # - unreviewed_package_read (READ permission)
        # - unreviewed_package_write (WRITE permission)
        # - unreviewed_package_all (ALL permission)
        # - group_package (via group membership)
        # Total: 5 packages
        self.assertEqual(payload["total_items"], 5)

        visible_uuids = {item["uuid"] for item in payload["items"]}
        expected_uuids = {
            str(self.reviewed_package.uuid),
            str(self.unreviewed_package_read.uuid),
            str(self.unreviewed_package_write.uuid),
            str(self.unreviewed_package_all.uuid),
            str(self.group_package.uuid),
        }
        self.assertEqual(visible_uuids, expected_uuids)

    def test_owner_sees_all_owned_packages(self):
        """Package owner sees all packages they created."""
        self.client.force_login(self.user1)
        response = self.client.get(self.ENDPOINT)

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        # user1 owns all packages
        # Total: 7 packages (all packages created in setUpTestData)
        self.assertEqual(payload["total_items"], 7)

    def test_filter_by_review_status_true(self):
        """Filter to show only reviewed packages."""
        self.client.force_login(self.user2)
        response = self.client.get(self.ENDPOINT, {"review_status": True})

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        # Only reviewed_package
        self.assertEqual(payload["total_items"], 1)
        self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))

    def test_filter_by_review_status_false(self):
        """Filter to show only unreviewed packages."""
        self.client.force_login(self.user2)
        response = self.client.get(self.ENDPOINT, {"review_status": False})

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        # user2's accessible unreviewed packages: 4
        self.assertEqual(payload["total_items"], 4)
        self.assertTrue(all(item["review_status"] == "unreviewed" for item in payload["items"]))

    def test_anonymous_filter_unreviewed_returns_empty(self):
        """Anonymous users get no results when filtering for unreviewed."""
        self.client.logout()
        response = self.client.get(self.ENDPOINT, {"review_status": False})

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        self.assertEqual(payload["total_items"], 0)


@tag("api", "end2end")
class GlobalCompoundListPermissionTest(APIPermissionTestBase):
    """
    Test permissions for /api/v1/compounds/ endpoint.

    This endpoint requires authentication.
    """

    ENDPOINT = "/api/v1/compounds/"

    def test_anonymous_user_cannot_access(self):
        """Anonymous users should get 401 Unauthorized."""
        self.client.logout()
        response = self.client.get(self.ENDPOINT)

        self.assertEqual(response.status_code, 401)

    def test_authenticated_user_sees_compounds_from_readable_packages(self):
        """Authenticated users see compounds from packages they can read."""
        self.client.force_login(self.user2)
        response = self.client.get(self.ENDPOINT)

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        self.assertEqual(payload["total_items"], 5)

        visible_uuids = {item["uuid"] for item in payload["items"]}
        expected_uuids = {
            str(self.reviewed_compound.uuid),
            str(self.read_compound.uuid),
            str(self.write_compound.uuid),
            str(self.all_compound.uuid),
            str(self.group_compound.uuid),
        }
        self.assertEqual(visible_uuids, expected_uuids)

    def test_user_without_permission_cannot_see_compound(self):
        """User without permission to package cannot see its compounds."""
        self.client.force_login(self.user3)
        response = self.client.get(self.ENDPOINT)

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        # user3 should only see compounds from reviewed_package
        self.assertEqual(payload["total_items"], 1)
        self.assertEqual(payload["items"][0]["uuid"], str(self.reviewed_compound.uuid))

    def test_owner_sees_all_compounds(self):
        """Package owner sees all compounds they created."""
        self.client.force_login(self.user1)
        response = self.client.get(self.ENDPOINT)

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        # user1 owns all packages, so sees all compounds
        self.assertEqual(payload["total_items"], 7)


@tag("api", "end2end")
class PackageScopedCompoundListPermissionTest(APIPermissionTestBase):
    """
    Test permissions for /api/v1/package/{uuid}/compound/ endpoint.

    This endpoint requires authentication AND package access.
    """

    def test_anonymous_user_cannot_access_reviewed_package(self):
        """Anonymous users should get 401 even for reviewed packages."""
        self.client.logout()
        endpoint = f"/api/v1/package/{self.reviewed_package.uuid}/compound/"
        response = self.client.get(endpoint)

        self.assertEqual(response.status_code, 401)

    def test_authenticated_user_can_access_reviewed_package(self):
        """Authenticated users can access reviewed packages."""
        self.client.force_login(self.user3)
        endpoint = f"/api/v1/package/{self.reviewed_package.uuid}/compound/"
        response = self.client.get(endpoint)

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        self.assertEqual(payload["total_items"], 1)
        self.assertEqual(payload["items"][0]["uuid"], str(self.reviewed_compound.uuid))

    def test_user_can_access_package_with_read_permission(self):
        """User with READ permission can access package-scoped endpoint."""
        self.client.force_login(self.user2)
        endpoint = f"/api/v1/package/{self.unreviewed_package_read.uuid}/compound/"
        response = self.client.get(endpoint)

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        self.assertEqual(payload["total_items"], 1)
        self.assertEqual(payload["items"][0]["uuid"], str(self.read_compound.uuid))

    def test_user_can_access_package_with_write_permission(self):
        """User with WRITE permission can access package-scoped endpoint."""
        self.client.force_login(self.user2)
        endpoint = f"/api/v1/package/{self.unreviewed_package_write.uuid}/compound/"
        response = self.client.get(endpoint)

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        self.assertEqual(payload["total_items"], 1)
        self.assertEqual(payload["items"][0]["uuid"], str(self.write_compound.uuid))

    def test_user_can_access_package_with_all_permission(self):
        """User with ALL permission can access package-scoped endpoint."""
        self.client.force_login(self.user2)
        endpoint = f"/api/v1/package/{self.unreviewed_package_all.uuid}/compound/"
        response = self.client.get(endpoint)

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        self.assertEqual(payload["total_items"], 1)
        self.assertEqual(payload["items"][0]["uuid"], str(self.all_compound.uuid))

    def test_user_cannot_access_package_without_permission(self):
        """User without permission gets 403 Forbidden."""
        self.client.force_login(self.user2)
        endpoint = f"/api/v1/package/{self.unreviewed_package_no_access.uuid}/compound/"
        response = self.client.get(endpoint)

        self.assertEqual(response.status_code, 403)

    def test_nonexistent_package_returns_404(self):
        """Request for non-existent package returns 404."""
        self.client.force_login(self.user2)
        fake_uuid = "00000000-0000-0000-0000-000000000000"
        endpoint = f"/api/v1/package/{fake_uuid}/compound/"
        response = self.client.get(endpoint)

        self.assertEqual(response.status_code, 404)

    def test_owner_can_access_owned_package(self):
        """Package owner can access their package."""
        self.client.force_login(self.user1)
        endpoint = f"/api/v1/package/{self.unreviewed_package_owned.uuid}/compound/"
        response = self.client.get(endpoint)

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        self.assertEqual(payload["total_items"], 1)
        self.assertEqual(payload["items"][0]["uuid"], str(self.owned_compound.uuid))

    def test_group_member_can_access_group_package(self):
        """Group member can access package via group permission."""
        self.client.force_login(self.user2)
        endpoint = f"/api/v1/package/{self.group_package.uuid}/compound/"
        response = self.client.get(endpoint)

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        self.assertEqual(payload["total_items"], 1)
        self.assertEqual(payload["items"][0]["uuid"], str(self.group_compound.uuid))

    def test_non_group_member_cannot_access_group_package(self):
        """Non-group member cannot access package with only group permission."""
        self.client.force_login(self.user3)
        endpoint = f"/api/v1/package/{self.group_package.uuid}/compound/"
        response = self.client.get(endpoint)

        self.assertEqual(response.status_code, 403)


@tag("api", "end2end")
class MultiResourcePermissionTest(APIPermissionTestBase):
    """
    Test that permission system works consistently across all resource types.

    Tests a sample of other endpoints to ensure permission logic is consistent.
    """

    def test_rules_endpoint_respects_permissions(self):
        """Rules endpoint uses same permission logic."""
        from epdb.models import SimpleAmbitRule

        # Create rule in no-access package
        rule = SimpleAmbitRule.create(
            self.unreviewed_package_no_access, "Test Rule", "Test", "[C:1]>>[C:1]O"
        )

        self.client.force_login(self.user2)
        response = self.client.get("/api/v1/rules/")

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        # user2 should not see the rule from no_access_package
        rule_uuids = [item["uuid"] for item in payload["items"]]
        self.assertNotIn(str(rule.uuid), rule_uuids)

    def test_reactions_endpoint_respects_permissions(self):
        """Reactions endpoint uses same permission logic."""
        from epdb.models import Reaction

        # Create reaction in no-access package
        reaction = Reaction.create(
            self.unreviewed_package_no_access, "Test Reaction", "Test", ["C"], ["CO"]
        )

        self.client.force_login(self.user2)
        response = self.client.get("/api/v1/reactions/")

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        # user2 should not see the reaction from no_access_package
        reaction_uuids = [item["uuid"] for item in payload["items"]]
        self.assertNotIn(str(reaction.uuid), reaction_uuids)

    def test_pathways_endpoint_respects_permissions(self):
        """Pathways endpoint uses same permission logic."""
        from epdb.models import Pathway

        # Create pathway in no-access package
        pathway = Pathway.objects.create(
            package=self.unreviewed_package_no_access, name="Test Pathway", description="Test"
        )

        self.client.force_login(self.user2)
        response = self.client.get("/api/v1/pathways/")

        self.assertEqual(response.status_code, 200)
        payload = response.json()

        # user2 should not see the pathway from no_access_package
        pathway_uuids = [item["uuid"] for item in payload["items"]]
        self.assertNotIn(str(pathway.uuid), pathway_uuids)
