"""
Test Annotation Permission Inheritance System

This test module validates that annotations inherit permissions from their parent
document and corpus objects, with document permissions taking precedence.

Permission Rules:
1. Document permissions are PRIMARY (most restrictive)
2. Corpus permissions are SECONDARY
3. Effective permission = MIN(document_permission, corpus_permission)
4. Structural annotations are always READ-ONLY if document is readable
5. Analysis visibility adds additional restrictions
"""

import logging

from django.contrib.auth import get_user_model
from django.core.files.base import ContentFile
from django.db import transaction
from django.test import TestCase
from graphene.test import Client
from graphql_relay import to_global_id

from config.graphql.schema import schema
from opencontractserver.annotations.models import (
    TOKEN_LABEL,
    Annotation,
    AnnotationLabel,
)
from opencontractserver.corpuses.models import Corpus
from opencontractserver.documents.models import Document, DocumentPath
from opencontractserver.tests.fixtures import SAMPLE_PDF_FILE_ONE_PATH
from opencontractserver.types.enums import PermissionTypes
from opencontractserver.utils.permissioning import (
    set_permissions_for_obj_to_user,
)

User = get_user_model()
logger = logging.getLogger(__name__)


class TestContext:
    """Mock context for GraphQL client"""

    def __init__(self, user):
        self.user = user


class AnnotationPermissionInheritanceTestCase(TestCase):
    """
    Tests the new annotation permission inheritance model where annotations
    inherit permissions from their parent document and corpus.
    """

    def setUp(self):
        """Set up test users, documents, corpuses, and annotations"""

        # Create test users
        self.user_alice = User.objects.create_user(username="Alice", password="test123")
        self.user_bob = User.objects.create_user(username="Bob", password="test123")
        self.user_charlie = User.objects.create_user(
            username="Charlie", password="test123"
        )
        self.superuser = User.objects.create_superuser(
            username="Super", password="admin"
        )

        # Create GraphQL clients for each user
        self.client_alice = Client(schema, context_value=TestContext(self.user_alice))
        self.client_bob = Client(schema, context_value=TestContext(self.user_bob))
        self.client_charlie = Client(
            schema, context_value=TestContext(self.user_charlie)
        )
        self.client_super = Client(schema, context_value=TestContext(self.superuser))

        # Create test documents
        self.doc_public = self._create_document(
            "Public Doc", self.user_alice, is_public=True
        )
        self.doc_private = self._create_document(
            "Private Doc", self.user_alice, is_public=False
        )
        self.doc_shared = self._create_document(
            "Shared Doc", self.user_alice, is_public=False
        )

        # Create test corpuses
        self.corpus_public = self._create_corpus(
            "Public Corpus", self.user_alice, is_public=True
        )
        self.corpus_private = self._create_corpus(
            "Private Corpus", self.user_alice, is_public=False
        )
        self.corpus_shared = self._create_corpus(
            "Shared Corpus", self.user_alice, is_public=False
        )

        # Create annotation labels
        self.label = AnnotationLabel.objects.create(
            label_type=TOKEN_LABEL, text="Test Label", creator=self.user_alice
        )

        # Set up initial permissions (will be modified per test)
        self._setup_base_permissions()

        # Create annotations for testing
        self._create_test_annotations()

    def _create_document(self, title, creator, is_public=False):
        """Helper to create a document with a real PDF"""
        with transaction.atomic():
            doc = Document.objects.create(
                title=title,
                description=f"Test document: {title}",
                creator=creator,
                is_public=is_public,
            )
            # Add a real PDF file from fixtures
            with SAMPLE_PDF_FILE_ONE_PATH.open("rb") as test_pdf:
                pdf_contents = ContentFile(test_pdf.read())
                doc.pdf_file.save("test.pdf", pdf_contents)
            return doc

    def _create_corpus(self, title, creator, is_public=False):
        """Helper to create a corpus"""
        return Corpus.objects.create(
            title=title,
            description=f"Test corpus: {title}",
            creator=creator,
            is_public=is_public,
        )

    def _setup_base_permissions(self):
        """Set up base permissions for testing"""
        # Alice owns everything (gets ALL permissions)
        set_permissions_for_obj_to_user(
            self.user_alice, self.doc_private, [PermissionTypes.ALL]
        )
        set_permissions_for_obj_to_user(
            self.user_alice, self.doc_shared, [PermissionTypes.ALL]
        )
        set_permissions_for_obj_to_user(
            self.user_alice, self.corpus_private, [PermissionTypes.ALL]
        )
        set_permissions_for_obj_to_user(
            self.user_alice, self.corpus_shared, [PermissionTypes.ALL]
        )

    def _create_test_annotations(self):
        """Create various types of annotations for testing"""
        # Structural annotation (always readable if document is readable)
        self.ann_structural = Annotation.objects.create(
            annotation_label=self.label,
            document=self.doc_private,
            corpus=None,  # No corpus for structural
            structural=True,
            creator=self.user_alice,
            page=1,
        )

        # Regular annotation in private corpus
        self.ann_private = Annotation.objects.create(
            annotation_label=self.label,
            document=self.doc_private,
            corpus=self.corpus_private,
            structural=False,
            creator=self.user_alice,
            page=1,
        )

        # Regular annotation in shared corpus
        self.ann_shared = Annotation.objects.create(
            annotation_label=self.label,
            document=self.doc_shared,
            corpus=self.corpus_shared,
            structural=False,
            creator=self.user_alice,
            page=1,
        )

    # =========================================================================
    # TEST SCENARIO 1: Document READ, No Corpus → Structural Only
    # =========================================================================

    def test_document_read_no_corpus_structural_only(self):
        """
        Scenario: User has READ on document but no corpus access
        Expected: Can see structural annotations only (READ-ONLY)
        """
        logger.info("\n" + "=" * 80)
        logger.info("TEST: Document READ, No Corpus → Structural Annotations Only")
        logger.info("=" * 80)

        # Give Bob READ access to private document only
        set_permissions_for_obj_to_user(
            self.user_bob, self.doc_private, [PermissionTypes.READ]
        )

        # Convert to global ID for GraphQL
        doc_global_id = to_global_id("DocumentType", self.doc_private.id)

        query = """
        query {
            annotations(documentId: "%s") {
                edges {
                    node {
                        id
                        structural
                        myPermissions
                        rawText
                    }
                }
            }
        }
        """ % doc_global_id

        result = self.client_bob.execute(query)

        # Check for errors
        if result.get("errors"):
            logger.error(f"GraphQL errors: {result['errors']}")

        annotations = result.get("data", {}).get("annotations", {}).get("edges", [])

        # Bob should see only structural annotations
        self.assertEqual(
            len(annotations), 1, "Should see exactly 1 structural annotation"
        )
        self.assertTrue(
            annotations[0]["node"]["structural"], "Should be structural annotation"
        )

        # Check permissions are READ-ONLY (backend format)
        permissions = annotations[0]["node"]["myPermissions"]
        self.assertIn("read_annotation", permissions, "Should have READ permission")
        self.assertNotIn(
            "update_annotation", permissions, "Should NOT have UPDATE permission"
        )
        self.assertNotIn(
            "create_annotation", permissions, "Should NOT have CREATE permission"
        )

        logger.info(
            "✓ User with document READ sees only structural annotations as READ-ONLY"
        )

    # =========================================================================
    # TEST SCENARIO 2: Document READ, Corpus READ → All Annotations READ-ONLY
    # =========================================================================

    def test_document_read_corpus_read(self):
        """
        Scenario: User has READ on both document and corpus
        Expected: Can see all corpus annotations (READ-ONLY)
        """
        logger.info("\n" + "=" * 80)
        logger.info("TEST: Document READ, Corpus READ → All Annotations READ-ONLY")
        logger.info("=" * 80)

        # Give Bob READ access to both document and corpus
        set_permissions_for_obj_to_user(
            self.user_bob, self.doc_shared, [PermissionTypes.READ]
        )
        set_permissions_for_obj_to_user(
            self.user_bob, self.corpus_shared, [PermissionTypes.READ]
        )

        # Add document to corpus via DocumentPath (new pattern)
        DocumentPath.objects.create(
            document=self.doc_shared,
            corpus=self.corpus_shared,
            path="/shared_doc.pdf",
            version_number=1,
            is_current=True,
            is_deleted=False,
            creator=self.user_alice,
        )

        # Convert to global IDs for GraphQL
        doc_global_id = to_global_id("DocumentType", self.doc_shared.id)
        corpus_global_id = to_global_id("CorpusType", self.corpus_shared.id)

        query = """
        query {{
            annotations(documentId: "{}", corpusId: "{}") {{
                edges {{
                    node {{
                        id
                        myPermissions
                        corpus {{
                            id
                        }}
                    }}
                }}
            }}
        }}
        """.format(doc_global_id, corpus_global_id)

        result = self.client_bob.execute(query)
        annotations = result.get("data", {}).get("annotations", {}).get("edges", [])

        # Should see corpus annotations
        self.assertGreater(len(annotations), 0, "Should see corpus annotations")

        # All should be READ-ONLY (backend format)
        for ann in annotations:
            permissions = ann["node"]["myPermissions"]
            self.assertIn("read_annotation", permissions, "Should have READ permission")
            self.assertNotIn(
                "update_annotation", permissions, "Should NOT have UPDATE permission"
            )

        logger.info(
            "✓ User with READ on both document and corpus sees annotations as READ-ONLY"
        )

    # =========================================================================
    # TEST SCENARIO 3: Document UPDATE, Corpus READ → Most Restrictive (READ)
    # =========================================================================

    def test_document_update_corpus_read_most_restrictive(self):
        """
        Scenario: User has UPDATE on document but only READ on corpus
        Expected: Annotations are READ-ONLY (most restrictive wins)
        """
        logger.info("\n" + "=" * 80)
        logger.info("TEST: Document UPDATE, Corpus READ → Most Restrictive (READ-ONLY)")
        logger.info("=" * 80)

        # Give Bob UPDATE on document but only READ on corpus
        set_permissions_for_obj_to_user(
            self.user_bob,
            self.doc_shared,
            [PermissionTypes.READ, PermissionTypes.UPDATE],
        )
        set_permissions_for_obj_to_user(
            self.user_bob,
            self.corpus_shared,
            [PermissionTypes.READ],  # Only READ, not UPDATE
        )

        # Add document to corpus via DocumentPath (new pattern)
        DocumentPath.objects.create(
            document=self.doc_shared,
            corpus=self.corpus_shared,
            path="/shared_doc.pdf",
            version_number=1,
            is_current=True,
            is_deleted=False,
            creator=self.user_alice,
        )

        # Convert to global IDs for GraphQL
        doc_global_id = to_global_id("DocumentType", self.doc_shared.id)
        corpus_global_id = to_global_id("CorpusType", self.corpus_shared.id)

        query = """
        query {{
            annotations(documentId: "{}", corpusId: "{}") {{
                edges {{
                    node {{
                        id
                        myPermissions
                    }}
                }}
            }}
        }}
        """.format(doc_global_id, corpus_global_id)

        result = self.client_bob.execute(query)
        annotations = result.get("data", {}).get("annotations", {}).get("edges", [])

        # Should have annotations
        self.assertGreater(len(annotations), 0, "Should see annotations")

        # Should be READ-ONLY despite UPDATE on document (backend format)
        for ann in annotations:
            permissions = ann["node"]["myPermissions"]
            self.assertIn("read_annotation", permissions, "Should have READ permission")
            self.assertNotIn(
                "update_annotation",
                permissions,
                "Should NOT have UPDATE (corpus restricts to READ)",
            )

        logger.info(
            "✓ Most restrictive permission (corpus READ) wins over document UPDATE"
        )

    # =========================================================================
    # TEST SCENARIO 4: Document READ, Corpus UPDATE → Most Restrictive (READ)
    # =========================================================================

    def test_document_read_corpus_update_most_restrictive(self):
        """
        Scenario: User has only READ on document but UPDATE on corpus
        Expected: Annotations are READ-ONLY (document restriction wins)
        """
        logger.info("\n" + "=" * 80)
        logger.info("TEST: Document READ, Corpus UPDATE → Most Restrictive (READ-ONLY)")
        logger.info("=" * 80)

        # Give Bob only READ on document but UPDATE on corpus
        set_permissions_for_obj_to_user(
            self.user_bob, self.doc_shared, [PermissionTypes.READ]  # Only READ
        )
        set_permissions_for_obj_to_user(
            self.user_bob,
            self.corpus_shared,
            [PermissionTypes.READ, PermissionTypes.UPDATE, PermissionTypes.CREATE],
        )

        # Add document to corpus via DocumentPath (new pattern)
        DocumentPath.objects.create(
            document=self.doc_shared,
            corpus=self.corpus_shared,
            path="/shared_doc.pdf",
            version_number=1,
            is_current=True,
            is_deleted=False,
            creator=self.user_alice,
        )

        # Convert to global IDs for GraphQL
        doc_global_id = to_global_id("DocumentType", self.doc_shared.id)
        corpus_global_id = to_global_id("CorpusType", self.corpus_shared.id)

        query = """
        query {{
            annotations(documentId: "{}", corpusId: "{}") {{
                edges {{
                    node {{
                        id
                        myPermissions
                    }}
                }}
            }}
        }}
        """.format(doc_global_id, corpus_global_id)

        result = self.client_bob.execute(query)
        annotations = result.get("data", {}).get("annotations", {}).get("edges", [])

        # Should have annotations
        self.assertGreater(len(annotations), 0, "Should see annotations")

        # Should be READ-ONLY despite UPDATE on corpus (backend format)
        for ann in annotations:
            permissions = ann["node"]["myPermissions"]
            self.assertIn("read_annotation", permissions, "Should have READ permission")
            self.assertNotIn(
                "update_annotation",
                permissions,
                "Should NOT have UPDATE (document restricts to READ)",
            )

        logger.info(
            "✓ Most restrictive permission (document READ) wins over corpus UPDATE"
        )

    # =========================================================================
    # TEST SCENARIO 5: Document UPDATE, Corpus UPDATE → Full Access
    # =========================================================================

    def test_document_update_corpus_update_full_access(self):
        """
        Scenario: User has UPDATE on both document and corpus
        Expected: Full access to annotations (CREATE, UPDATE, DELETE)
        """
        logger.info("\n" + "=" * 80)
        logger.info("TEST: Document UPDATE, Corpus UPDATE → Full Access")
        logger.info("=" * 80)

        # Give Bob UPDATE on both document and corpus
        set_permissions_for_obj_to_user(
            self.user_bob,
            self.doc_shared,
            [PermissionTypes.READ, PermissionTypes.UPDATE, PermissionTypes.CREATE],
        )
        set_permissions_for_obj_to_user(
            self.user_bob,
            self.corpus_shared,
            [PermissionTypes.READ, PermissionTypes.UPDATE, PermissionTypes.CREATE],
        )

        # Add document to corpus via DocumentPath (new pattern)
        DocumentPath.objects.create(
            document=self.doc_shared,
            corpus=self.corpus_shared,
            path="/shared_doc.pdf",
            version_number=1,
            is_current=True,
            is_deleted=False,
            creator=self.user_alice,
        )

        # Convert to global IDs for GraphQL
        doc_global_id = to_global_id("DocumentType", self.doc_shared.id)
        corpus_global_id = to_global_id("CorpusType", self.corpus_shared.id)

        query = """
        query {{
            annotations(documentId: "{}", corpusId: "{}") {{
                edges {{
                    node {{
                        id
                        myPermissions
                    }}
                }}
            }}
        }}
        """.format(doc_global_id, corpus_global_id)

        result = self.client_bob.execute(query)
        annotations = result.get("data", {}).get("annotations", {}).get("edges", [])

        # Should have annotations
        self.assertGreater(len(annotations), 0, "Should see annotations")

        # Should have full permissions (backend format)
        for ann in annotations:
            permissions = ann["node"]["myPermissions"]
            self.assertIn("read_annotation", permissions, "Should have READ permission")
            self.assertIn(
                "update_annotation", permissions, "Should have UPDATE permission"
            )
            self.assertIn(
                "create_annotation", permissions, "Should have CREATE permission"
            )

        logger.info(
            "✓ User with UPDATE on both document and corpus has full annotation access"
        )

    # =========================================================================
    # TEST SCENARIO 6: No Document Permission → No Access
    # =========================================================================

    def test_no_document_permission_no_access(self):
        """
        Scenario: User has NO permission on document (even with corpus permission)
        Expected: Cannot see any annotations
        """
        logger.info("\n" + "=" * 80)
        logger.info("TEST: No Document Permission → No Access")
        logger.info("=" * 80)

        # Give Bob corpus permission but NO document permission
        set_permissions_for_obj_to_user(
            self.user_bob,
            self.corpus_shared,
            [PermissionTypes.ALL],  # Even with ALL on corpus
        )
        # Explicitly NO permissions on document

        # Add document to corpus via DocumentPath (new pattern)
        DocumentPath.objects.create(
            document=self.doc_shared,
            corpus=self.corpus_shared,
            path="/shared_doc.pdf",
            version_number=1,
            is_current=True,
            is_deleted=False,
            creator=self.user_alice,
        )

        # Convert to global IDs for GraphQL
        doc_global_id = to_global_id("DocumentType", self.doc_shared.id)
        corpus_global_id = to_global_id("CorpusType", self.corpus_shared.id)

        query = """
        query {{
            annotations(documentId: "{}", corpusId: "{}") {{
                edges {{
                    node {{
                        id
                    }}
                }}
            }}
        }}
        """.format(doc_global_id, corpus_global_id)

        result = self.client_bob.execute(query)
        annotations = result.get("data", {}).get("annotations", {}).get("edges", [])

        # Should see NO annotations without document permission
        self.assertEqual(
            len(annotations),
            0,
            "Should NOT see annotations without document permission",
        )

        logger.info(
            "✓ No document permission means no annotation access (even with corpus permission)"
        )

    # =========================================================================
    # TEST SCENARIO 7: Superuser Access
    # =========================================================================

    def test_superuser_gets_normal_inherited_access(self):
        """
        Scenario: Superuser accessing annotations on a PRIVATE stranger
        document/corpus it has NOT been granted.

        Under the scoped-admin contract (2026-05), superusers are computed like
        any normal user for DATA authorization — there is no blanket bypass. A
        no-grant superuser therefore gets the SAME inherited annotation
        visibility a normal user would: it is DENIED on a private stranger
        document/corpus (the structural annotation is only "always visible" when
        the document itself is readable, which it is not here).

        A positive case then grants the superuser READ on the document and
        corpus and proves access flows through the NORMAL inherited path
        (parity with a normal reader). (The fixture's structural annotation has
        ``corpus=None`` and is not linked to the document's structural set, so
        in this doc+corpus query mode it is filtered out for everyone — its
        doc-only visibility is pinned by
        ``test_document_read_no_corpus_structural_only``.)
        """
        logger.info("\n" + "=" * 80)
        logger.info("TEST: Superuser → Normal Inherited Access (no bypass)")
        logger.info("=" * 80)

        # Add document to corpus via DocumentPath (new pattern)
        DocumentPath.objects.create(
            document=self.doc_private,
            corpus=self.corpus_private,
            path="/private_doc.pdf",
            version_number=1,
            is_current=True,
            is_deleted=False,
            creator=self.user_alice,
        )

        # Convert to global IDs for GraphQL
        doc_global_id = to_global_id("DocumentType", self.doc_private.id)
        corpus_global_id = to_global_id("CorpusType", self.corpus_private.id)

        query = """
        query {{
            annotations(documentId: "{}", corpusId: "{}") {{
                edges {{
                    node {{
                        id
                        structural
                        myPermissions
                    }}
                }}
            }}
        }}
        """.format(doc_global_id, corpus_global_id)

        # --- No-grant superuser is DENIED (normal inherited perms) -----------
        result = self.client_super.execute(query)
        annotations = result.get("data", {}).get("annotations", {}).get("edges", [])
        self.assertEqual(
            len(annotations),
            0,
            "No-grant superuser must NOT see annotations on a private stranger "
            "document/corpus (no blanket bypass)",
        )
        logger.info("✓ No-grant superuser denied on private stranger doc/corpus")

        # --- Positive case: grant READ → access via the NORMAL path ----------
        set_permissions_for_obj_to_user(
            self.superuser, self.doc_private, [PermissionTypes.READ]
        )
        set_permissions_for_obj_to_user(
            self.superuser, self.corpus_private, [PermissionTypes.READ]
        )

        result = self.client_super.execute(query)
        annotations = result.get("data", {}).get("annotations", {}).get("edges", [])
        self.assertGreater(
            len(annotations),
            0,
            "After READ grant, superuser should see annotations via the normal path",
        )

        # Parity with a normal reader: READ is present (no synthetic full-CRUD
        # fold-in for superusers on data objects).
        for ann in annotations:
            permissions = ann["node"]["myPermissions"]
            self.assertIn(
                "read_annotation",
                permissions,
                "Granted superuser should have READ on visible annotations",
            )

        logger.info(
            "✓ Superuser reaches annotations via normal inherited READ (parity)"
        )

    # =========================================================================
    # TEST SCENARIO 8: Permission Format Compatibility
    # =========================================================================

    def test_permission_format_compatibility(self):
        """
        Scenario: Verify permission format matches frontend expectations
        Expected: Permissions returned as CAN_* format, not database format
        """
        logger.info("\n" + "=" * 80)
        logger.info("TEST: Permission Format Frontend Compatibility")
        logger.info("=" * 80)

        # Add document to corpus via DocumentPath (new pattern)
        DocumentPath.objects.create(
            document=self.doc_private,
            corpus=self.corpus_private,
            path="/private_doc.pdf",
            version_number=1,
            is_current=True,
            is_deleted=False,
            creator=self.user_alice,
        )

        # Convert to global IDs for GraphQL
        doc_global_id = to_global_id("DocumentType", self.doc_private.id)
        corpus_global_id = to_global_id("CorpusType", self.corpus_private.id)

        # Give Alice's annotations full permissions (she owns them)
        query = """
        query {{
            annotations(documentId: "{}", corpusId: "{}") {{
                edges {{
                    node {{
                        id
                        myPermissions
                    }}
                }}
            }}
        }}
        """.format(doc_global_id, corpus_global_id)

        result = self.client_alice.execute(query)
        annotations = result.get("data", {}).get("annotations", {}).get("edges", [])

        # Check format is CAN_* not read_annotation, update_annotation, etc.
        for ann in annotations:
            permissions = ann["node"]["myPermissions"]

            # Should have backend format
            for perm in permissions:
                self.assertTrue(
                    perm
                    in [
                        "superuser",
                        "read_annotation",
                        "update_annotation",
                        "create_annotation",
                        "remove_annotation",
                        "publish_annotation",
                        "permission_annotation",
                    ]
                    or perm.endswith("_annotation"),
                    f"Permission '{perm}' should be in backend format",
                )

            # Backend format check
            valid_formats = [
                "read_annotation",
                "create_annotation",
                "update_annotation",
                "remove_annotation",
                "publish_annotation",
                "permission_annotation",
            ]
            # Check that at least some permissions are in the expected format
            has_valid_format = any(perm in valid_formats for perm in permissions)

            self.assertTrue(
                has_valid_format,
                "Permissions should include backend-format entries like read_annotation",
            )
            # For now, we'll be lenient with the format check since the system may still be transitioning
            logger.info(f"Permissions received: {permissions}")

        logger.info("✓ Permissions returned (format compatibility checked)")
