from django.test import TestCase, tag

from epdb.logic import PackageManager, UserManager
from epdb.models import Compound, Reaction, Pathway, EPModel, SimpleAmbitRule, Scenario


class BaseTestAPIGetPaginated:
    """
    Mixin class for API pagination tests.

    Subclasses must inherit from both this class and TestCase, e.g.:
        class MyTest(BaseTestAPIGetPaginated, TestCase):
            ...

    Subclasses must define:
    - resource_name: Singular name (e.g., "compound")
    - resource_name_plural: Plural name (e.g., "compounds")
    - global_endpoint: Global listing endpoint (e.g., "/api/v1/compounds/")
    - package_endpoint_template: Template for package-scoped endpoint or None
    - total_reviewed: Number of reviewed items to create
    - total_unreviewed: Number of unreviewed items to create
    - create_reviewed_resource(cls, package, idx): Factory method
    - create_unreviewed_resource(cls, package, idx): Factory method
    """

    # Configuration to be overridden by subclasses
    resource_name = None
    resource_name_plural = None
    global_endpoint = None
    package_endpoint_template = None
    total_reviewed = 50
    total_unreviewed = 20
    default_page_size = 50
    max_page_size = 100

    @classmethod
    def setUpTestData(cls):
        # Create test user
        cls.user = UserManager.create_user(
            f"{cls.resource_name}-user",
            f"{cls.resource_name}-user@envipath.com",
            "SuperSafe",
            set_setting=False,
            add_to_group=False,
            is_active=True,
        )

        # Delete the auto-created default package to ensure clean test data
        default_pkg = cls.user.default_package
        cls.user.default_package = None
        cls.user.save()
        default_pkg.delete()

        # Create reviewed package
        cls.reviewed_package = PackageManager.create_package(
            cls.user, "Reviewed Package", f"Reviewed package for {cls.resource_name} tests"
        )
        cls.reviewed_package.reviewed = True
        cls.reviewed_package.save()

        # Create unreviewed package
        cls.unreviewed_package = PackageManager.create_package(
            cls.user, "Draft Package", f"Unreviewed package for {cls.resource_name} tests"
        )

        # Create reviewed resources
        for idx in range(cls.total_reviewed):
            cls.create_reviewed_resource(cls.reviewed_package, idx)

        # Create unreviewed resources
        for idx in range(cls.total_unreviewed):
            cls.create_unreviewed_resource(cls.unreviewed_package, idx)

        # Set up package-scoped endpoints if applicable
        if cls.package_endpoint_template:
            cls.reviewed_package_endpoint = cls.package_endpoint_template.format(
                uuid=cls.reviewed_package.uuid
            )
            cls.unreviewed_package_endpoint = cls.package_endpoint_template.format(
                uuid=cls.unreviewed_package.uuid
            )

    @classmethod
    def create_reviewed_resource(cls, package, idx):
        """
        Create a single reviewed resource.
        Must be implemented by subclass.

        Args:
            package: The package to create the resource in
            idx: Index of the resource (0-based)
        """
        raise NotImplementedError(f"{cls.__name__} must implement create_reviewed_resource()")

    @classmethod
    def create_unreviewed_resource(cls, package, idx):
        """
        Create a single unreviewed resource.
        Must be implemented by subclass.

        Args:
            package: The package to create the resource in
            idx: Index of the resource (0-based)
        """
        raise NotImplementedError(f"{cls.__name__} must implement create_unreviewed_resource()")

    def setUp(self):
        self.client.force_login(self.user)

    def test_requires_session_authentication(self):
        """Test that the global endpoint requires authentication."""
        self.client.logout()
        response = self.client.get(self.global_endpoint)
        self.assertEqual(response.status_code, 401)

    def test_global_listing_uses_default_page_size(self):
        """Test that the global endpoint uses default pagination settings."""
        response = self.client.get(self.global_endpoint, {"review_status": True})
        self.assertEqual(response.status_code, 200)

        payload = response.json()
        self.assertEqual(payload["page"], 1)
        self.assertEqual(payload["page_size"], self.default_page_size)
        self.assertEqual(payload["total_items"], self.total_reviewed)

        # Verify only reviewed items are returned
        self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))

    def test_can_request_later_page(self):
        """Test that pagination works for later pages."""
        if self.total_reviewed <= self.default_page_size:
            self.skipTest(
                f"Not enough items to test pagination "
                f"({self.total_reviewed} <= {self.default_page_size})"
            )

        response = self.client.get(self.global_endpoint, {"page": 2, "review_status": True})
        self.assertEqual(response.status_code, 200)

        payload = response.json()
        self.assertEqual(payload["page"], 2)

        # Calculate expected items on page 2
        expected_items = min(self.default_page_size, self.total_reviewed - self.default_page_size)
        self.assertEqual(len(payload["items"]), expected_items)

        # Verify only reviewed items are returned
        self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))

    def test_page_size_is_capped(self):
        """Test that page size is capped at the maximum."""
        if self.total_reviewed <= self.max_page_size:
            self.skipTest(
                f"Not enough items to test page size cap "
                f"({self.total_reviewed} <= {self.max_page_size})"
            )

        response = self.client.get(self.global_endpoint, {"page_size": 150})
        self.assertEqual(response.status_code, 200)

        payload = response.json()
        self.assertEqual(payload["page_size"], self.max_page_size)
        self.assertEqual(len(payload["items"]), self.max_page_size)

    def test_package_endpoint_for_reviewed_package(self):
        """Test the package-scoped endpoint for reviewed packages."""
        if not self.package_endpoint_template:
            self.skipTest("No package endpoint for this resource")

        response = self.client.get(self.reviewed_package_endpoint)
        self.assertEqual(response.status_code, 200)

        payload = response.json()
        self.assertEqual(payload["total_items"], self.total_reviewed)

        # Verify only reviewed items are returned
        self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))

    def test_package_endpoint_for_unreviewed_package(self):
        """Test the package-scoped endpoint for unreviewed packages."""
        if not self.package_endpoint_template:
            self.skipTest("No package endpoint for this resource")

        response = self.client.get(self.unreviewed_package_endpoint)
        self.assertEqual(response.status_code, 200)

        payload = response.json()
        self.assertEqual(payload["total_items"], self.total_unreviewed)

        # Verify only unreviewed items are returned
        self.assertTrue(all(item["review_status"] == "unreviewed" for item in payload["items"]))


@tag("api", "end2end")
class PackagePaginationAPITest(TestCase):
    ENDPOINT = "/api/v1/packages/"

    @classmethod
    def setUpTestData(cls):
        cls.user = UserManager.create_user(
            "package-user",
            "package-user@envipath.com",
            "SuperSafe",
            set_setting=False,
            add_to_group=False,
            is_active=True,
        )

        # Delete the auto-created default package to ensure clean test data
        default_pkg = cls.user.default_package
        cls.user.default_package = None
        cls.user.save()
        default_pkg.delete()

        # Create reviewed packages
        cls.total_reviewed = 25
        for idx in range(cls.total_reviewed):
            package = PackageManager.create_package(
                cls.user, f"Reviewed Package {idx:03d}", "Reviewed package for tests"
            )
            package.reviewed = True
            package.save()

        # Create unreviewed packages
        cls.total_unreviewed = 15
        for idx in range(cls.total_unreviewed):
            PackageManager.create_package(
                cls.user, f"Draft Package {idx:03d}", "Unreviewed package for tests"
            )

    def setUp(self):
        self.client.force_login(self.user)

    def test_anonymous_can_access_reviewed_packages(self):
        self.client.logout()
        response = self.client.get(self.ENDPOINT)
        self.assertEqual(response.status_code, 200)

        payload = response.json()
        # Anonymous users can only see reviewed packages
        self.assertEqual(payload["total_items"], self.total_reviewed)
        self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))

    def test_listing_uses_default_page_size(self):
        response = self.client.get(self.ENDPOINT)
        self.assertEqual(response.status_code, 200)

        payload = response.json()
        self.assertEqual(payload["page"], 1)
        self.assertEqual(payload["page_size"], 50)
        self.assertEqual(payload["total_items"], self.total_reviewed + self.total_unreviewed)

    def test_reviewed_filter_true(self):
        response = self.client.get(self.ENDPOINT, {"review_status": True})
        self.assertEqual(response.status_code, 200)

        payload = response.json()
        self.assertEqual(payload["total_items"], self.total_reviewed)
        self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))

    def test_reviewed_filter_false(self):
        response = self.client.get(self.ENDPOINT, {"review_status": False})
        self.assertEqual(response.status_code, 200)

        payload = response.json()
        self.assertEqual(payload["total_items"], self.total_unreviewed)
        self.assertTrue(all(item["review_status"] == "unreviewed" for item in payload["items"]))

    def test_reviewed_filter_false_anonymous(self):
        self.client.logout()
        response = self.client.get(self.ENDPOINT, {"review_status": False})
        self.assertEqual(response.status_code, 200)

        payload = response.json()
        # Anonymous users cannot access unreviewed packages
        self.assertEqual(payload["total_items"], 0)


@tag("api", "end2end")
class CompoundPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
    """Compound pagination tests using base class."""

    resource_name = "compound"
    resource_name_plural = "compounds"
    global_endpoint = "/api/v1/compounds/"
    package_endpoint_template = "/api/v1/package/{uuid}/compound/"
    total_reviewed = 125
    total_unreviewed = 35

    @classmethod
    def create_reviewed_resource(cls, package, idx):
        simple_smiles = ["C", "CC", "CCC", "CCCC", "CCCCC"]
        smiles = simple_smiles[idx % len(simple_smiles)] + ("O" * (idx // len(simple_smiles)))
        return Compound.create(
            package,
            smiles,
            name=f"Reviewed Compound {idx:03d}",
            description="Compound for pagination tests",
        )

    @classmethod
    def create_unreviewed_resource(cls, package, idx):
        simple_smiles = ["C", "CC", "CCC", "CCCC", "CCCCC"]
        smiles = simple_smiles[idx % len(simple_smiles)] + ("N" * (idx // len(simple_smiles)))
        return Compound.create(
            package,
            smiles,
            name=f"Draft Compound {idx:03d}",
            description="Compound for pagination tests",
        )


@tag("api", "end2end")
class RulePaginationAPITest(BaseTestAPIGetPaginated, TestCase):
    """Rule pagination tests using base class."""

    resource_name = "rule"
    resource_name_plural = "rules"
    global_endpoint = "/api/v1/rules/"
    package_endpoint_template = "/api/v1/package/{uuid}/rule/"
    total_reviewed = 125
    total_unreviewed = 35

    @classmethod
    def create_reviewed_resource(cls, package, idx):
        # Create unique SMIRKS by combining chain length and functional group variations
        # This ensures each idx gets a truly unique SMIRKS pattern
        carbon_chain = "C" * (idx + 1)  # C, CC, CCC, ... (grows with idx)
        smirks = f"[{carbon_chain}:1]>>[{carbon_chain}:1]O"
        return SimpleAmbitRule.create(
            package,
            f"Reviewed Rule {idx:03d}",
            f"Rule {idx} for pagination tests",
            smirks,
        )

    @classmethod
    def create_unreviewed_resource(cls, package, idx):
        # Create unique SMIRKS by varying the carbon chain length
        carbon_chain = "C" * (idx + 1)  # C, CC, CCC, ... (grows with idx)
        smirks = f"[{carbon_chain}:1]>>[{carbon_chain}:1]N"
        return SimpleAmbitRule.create(
            package,
            f"Draft Rule {idx:03d}",
            f"Rule {idx} for pagination tests",
            smirks,
        )


@tag("api", "end2end")
class ReactionPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
    """Reaction pagination tests using base class."""

    resource_name = "reaction"
    resource_name_plural = "reactions"
    global_endpoint = "/api/v1/reactions/"
    package_endpoint_template = "/api/v1/package/{uuid}/reaction/"
    total_reviewed = 125
    total_unreviewed = 35

    @classmethod
    def create_reviewed_resource(cls, package, idx):
        # Generate unique SMILES with growing chain lengths to avoid duplicates
        # Each idx gets a unique chain length
        educt_smiles = "C" * (idx + 1)  # C, CC, CCC, ... (grows with idx)
        product_smiles = educt_smiles + "O"
        return Reaction.create(
            package=package,
            name=f"Reviewed Reaction {idx:03d}",
            description="Reaction for pagination tests",
            educts=[educt_smiles],
            products=[product_smiles],
        )

    @classmethod
    def create_unreviewed_resource(cls, package, idx):
        # Generate unique SMILES with growing chain lengths to avoid duplicates
        # Each idx gets a unique chain length
        educt_smiles = "C" * (idx + 1)  # C, CC, CCC, ... (grows with idx)
        product_smiles = educt_smiles + "N"
        return Reaction.create(
            package=package,
            name=f"Draft Reaction {idx:03d}",
            description="Reaction for pagination tests",
            educts=[educt_smiles],
            products=[product_smiles],
        )


@tag("api", "end2end")
class PathwayPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
    """Pathway pagination tests using base class."""

    resource_name = "pathway"
    resource_name_plural = "pathways"
    global_endpoint = "/api/v1/pathways/"
    package_endpoint_template = "/api/v1/package/{uuid}/pathway/"
    total_reviewed = 125
    total_unreviewed = 35

    @classmethod
    def create_reviewed_resource(cls, package, idx):
        return Pathway.objects.create(
            package=package,
            name=f"Reviewed Pathway {idx:03d}",
            description="Pathway for pagination tests",
        )

    @classmethod
    def create_unreviewed_resource(cls, package, idx):
        return Pathway.objects.create(
            package=package,
            name=f"Draft Pathway {idx:03d}",
            description="Pathway for pagination tests",
        )


@tag("api", "end2end")
class ModelPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
    """Model pagination tests using base class."""

    resource_name = "model"
    resource_name_plural = "models"
    global_endpoint = "/api/v1/models/"
    package_endpoint_template = "/api/v1/package/{uuid}/model/"
    total_reviewed = 125
    total_unreviewed = 35

    @classmethod
    def create_reviewed_resource(cls, package, idx):
        return EPModel.objects.create(
            package=package,
            name=f"Reviewed Model {idx:03d}",
            description="Model for pagination tests",
        )

    @classmethod
    def create_unreviewed_resource(cls, package, idx):
        return EPModel.objects.create(
            package=package,
            name=f"Draft Model {idx:03d}",
            description="Model for pagination tests",
        )


@tag("api", "end2end")
class ScenarioPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
    """Scenario pagination tests using base class."""

    resource_name = "scenario"
    resource_name_plural = "scenarios"
    global_endpoint = "/api/v1/scenarios/"
    package_endpoint_template = "/api/v1/package/{uuid}/scenario/"
    total_reviewed = 125
    total_unreviewed = 35

    @classmethod
    def create_reviewed_resource(cls, package, idx):
        return Scenario.create(
            package,
            f"Reviewed Scenario {idx:03d}",
            "Scenario for pagination tests",
            "2025-01-01",
            "lab",
            [],
        )

    @classmethod
    def create_unreviewed_resource(cls, package, idx):
        return Scenario.create(
            package,
            f"Draft Scenario {idx:03d}",
            "Scenario for pagination tests",
            "2025-01-01",
            "field",
            [],
        )
