from unittest.mock import MagicMock, PropertyMock, patch

from django.conf import settings as s
from django.test import TestCase, override_settings

from epdb.logic import PackageManager
from epdb.models import SimpleAmbitRule, User
from utilities.chem import FormatConverter


@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models", CELERY_TASK_ALWAYS_EAGER=True)
class SimpleAmbitRuleTest(TestCase):
    fixtures = ["test_fixtures_incl_model.jsonl.gz"]

    @classmethod
    def setUpClass(cls):
        super(SimpleAmbitRuleTest, cls).setUpClass()
        cls.user = User.objects.get(username="anonymous")
        cls.package = PackageManager.create_package(
            cls.user, "Simple Ambit Rule Test Package", "Test Package for SimpleAmbitRule"
        )

    def test_create_basic_rule(self):
        """Test creating a basic SimpleAmbitRule with minimal parameters."""
        smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"

        rule = SimpleAmbitRule.create(package=self.package, smirks=smirks)

        self.assertIsInstance(rule, SimpleAmbitRule)
        self.assertEqual(rule.smirks, smirks)
        self.assertEqual(rule.package, self.package)
        self.assertRegex(rule.name, r"Rule \d+")
        self.assertEqual(rule.description, "no description")
        self.assertIsNone(rule.reactant_filter_smarts)
        self.assertIsNone(rule.product_filter_smarts)

    def test_create_with_all_parameters(self):
        """Test creating SimpleAmbitRule with all parameters."""
        smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
        name = "Test Rule"
        description = "A test biotransformation rule"
        reactant_filter = "[CH2X4]"
        product_filter = "[OH]"

        rule = SimpleAmbitRule.create(
            package=self.package,
            name=name,
            description=description,
            smirks=smirks,
            reactant_filter_smarts=reactant_filter,
            product_filter_smarts=product_filter,
        )

        self.assertEqual(rule.name, name)
        self.assertEqual(rule.description, description)
        self.assertEqual(rule.smirks, smirks)
        self.assertEqual(rule.reactant_filter_smarts, reactant_filter)
        self.assertEqual(rule.product_filter_smarts, product_filter)

    def test_smirks_required(self):
        """Test that SMIRKS is required for rule creation."""
        with self.assertRaises(ValueError) as cm:
            SimpleAmbitRule.create(package=self.package, smirks=None)
        self.assertIn("SMIRKS is required", str(cm.exception))

        with self.assertRaises(ValueError) as cm:
            SimpleAmbitRule.create(package=self.package, smirks="")
        self.assertIn("SMIRKS is required", str(cm.exception))

        with self.assertRaises(ValueError) as cm:
            SimpleAmbitRule.create(package=self.package, smirks="   ")
        self.assertIn("SMIRKS is required", str(cm.exception))

    @patch("epdb.models.FormatConverter.is_valid_smirks")
    def test_invalid_smirks_validation(self, mock_is_valid):
        """Test validation of SMIRKS format."""
        mock_is_valid.return_value = False

        invalid_smirks = "invalid_smirks_string"
        with self.assertRaises(ValueError) as cm:
            SimpleAmbitRule.create(package=self.package, smirks=invalid_smirks)

        self.assertIn(f'SMIRKS "{invalid_smirks}" is invalid', str(cm.exception))
        mock_is_valid.assert_called_once_with(invalid_smirks)

    def test_smirks_trimming(self):
        """Test that SMIRKS strings are trimmed during creation."""
        smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
        smirks_with_whitespace = f"   {smirks}   "

        rule = SimpleAmbitRule.create(package=self.package, smirks=smirks_with_whitespace)

        self.assertEqual(rule.smirks, smirks)

    def test_empty_name_and_description_handling(self):
        """Test that empty name and description are handled appropriately."""
        smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"

        rule = SimpleAmbitRule.create(
            package=self.package, smirks=smirks, name="", description="   "
        )

        self.assertRegex(rule.name, r"Rule \d+")
        self.assertEqual(rule.description, "no description")

    def test_deduplication_basic(self):
        """Test that identical rules are deduplicated."""
        smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"

        rule1 = SimpleAmbitRule.create(package=self.package, smirks=smirks, name="Rule 1")

        rule2 = SimpleAmbitRule.create(
            package=self.package,
            smirks=smirks,
            name="Rule 2",  # Different name, but same SMIRKS
        )

        self.assertEqual(rule1.pk, rule2.pk)
        self.assertEqual(
            SimpleAmbitRule.objects.filter(package=self.package, smirks=smirks).count(), 1
        )

    def test_deduplication_with_filters(self):
        """Test deduplication with filter SMARTS."""
        smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
        reactant_filter = "[CH2X4]"
        product_filter = "[OH]"

        rule1 = SimpleAmbitRule.create(
            package=self.package,
            smirks=smirks,
            reactant_filter_smarts=reactant_filter,
            product_filter_smarts=product_filter,
        )

        rule2 = SimpleAmbitRule.create(
            package=self.package,
            smirks=smirks,
            reactant_filter_smarts=reactant_filter,
            product_filter_smarts=product_filter,
        )

        self.assertEqual(rule1.pk, rule2.pk)

    def test_no_deduplication_different_filters(self):
        """Test that rules with different filters are not deduplicated."""
        smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"

        rule1 = SimpleAmbitRule.create(
            package=self.package, smirks=smirks, reactant_filter_smarts="[CH2X4]"
        )

        rule2 = SimpleAmbitRule.create(
            package=self.package, smirks=smirks, reactant_filter_smarts="[CH3X4]"
        )

        self.assertNotEqual(rule1.pk, rule2.pk)
        self.assertEqual(
            SimpleAmbitRule.objects.filter(package=self.package, smirks=smirks).count(), 2
        )

    def test_filter_smarts_trimming(self):
        """Test that filter SMARTS are trimmed and handled correctly."""
        smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"

        # Test with whitespace-only filters (should be treated as None)
        rule = SimpleAmbitRule.create(
            package=self.package,
            smirks=smirks,
            reactant_filter_smarts="   ",
            product_filter_smarts="   ",
        )

        self.assertIsNone(rule.reactant_filter_smarts)
        self.assertIsNone(rule.product_filter_smarts)

    def test_url_property(self):
        """Test the URL property generation."""
        rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")

        expected_url = f"{self.package.url}/simple-ambit-rule/{rule.uuid}"
        self.assertEqual(rule.url, expected_url)

    @patch("epdb.models.FormatConverter.apply")
    def test_apply_method(self, mock_apply):
        """Test the apply method delegates to FormatConverter."""
        mock_apply.return_value = ["product1", "product2"]

        rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")

        test_smiles = "CCO"
        result = rule.apply(test_smiles)

        mock_apply.assert_called_once_with(
            test_smiles, rule.smirks, reactant_filter_smarts=None, product_filter_smarts=None
        )
        self.assertEqual(result, ["product1", "product2"])

    def test_reactants_smarts_property(self):
        """Test reactants_smarts property extracts correct part of SMIRKS."""
        smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
        expected_reactants = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]"

        rule = SimpleAmbitRule.create(package=self.package, smirks=smirks)

        self.assertEqual(rule.reactants_smarts, expected_reactants)

    def test_products_smarts_property(self):
        """Test products_smarts property extracts correct part of SMIRKS."""
        smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
        expected_products = "[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"

        rule = SimpleAmbitRule.create(package=self.package, smirks=smirks)

        self.assertEqual(rule.products_smarts, expected_products)

    @patch(f"{s.EPDB_PACKAGE_MODEL.replace('.', '.models.')}.objects")
    def test_related_reactions_property(self, mock_package_objects):
        """Test related_reactions property returns correct queryset."""
        mock_qs = MagicMock()
        mock_package_objects.filter.return_value = mock_qs

        rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")

        # Instead of directly assigning, patch the property or use with patch.object
        with patch.object(
            type(rule), "reaction_rule", new_callable=PropertyMock
        ) as mock_reaction_rule:
            mock_reaction_rule.return_value.filter.return_value.order_by.return_value = [
                "reaction1",
                "reaction2",
            ]

            result = rule.related_reactions

            mock_package_objects.filter.assert_called_once_with(reviewed=True)
            mock_reaction_rule.return_value.filter.assert_called_once_with(package__in=mock_qs)
            mock_reaction_rule.return_value.filter.return_value.order_by.assert_called_once_with(
                "name"
            )
            self.assertEqual(result, ["reaction1", "reaction2"])

    @patch("epdb.models.Pathway.objects")
    @patch("epdb.models.Edge.objects")
    def test_related_pathways_property(self, mock_edge_objects, mock_pathway_objects):
        """Test related_pathways property returns correct queryset."""

        mock_related_reactions = ["reaction1", "reaction2"]

        with patch.object(
            SimpleAmbitRule, "related_reactions", new_callable=PropertyMock
        ) as mock_prop:
            mock_prop.return_value = mock_related_reactions

            rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")

            # Mock Edge objects query
            mock_edge_values = MagicMock()
            mock_edge_values.values.return_value = ["pathway_id1", "pathway_id2"]
            mock_edge_objects.filter.return_value = mock_edge_values

            # Mock Pathway objects query
            mock_pathway_qs = MagicMock()
            mock_pathway_objects.filter.return_value.order_by.return_value = mock_pathway_qs

            result = rule.related_pathways

            mock_edge_objects.filter.assert_called_once_with(edge_label__in=mock_related_reactions)
            mock_edge_values.values.assert_called_once_with("pathway_id")
            mock_pathway_objects.filter.assert_called_once()
            self.assertEqual(result, mock_pathway_qs)

    @patch("epdb.models.IndigoUtils.smirks_to_svg")
    def test_as_svg_property(self, mock_smirks_to_svg):
        """Test as_svg property calls IndigoUtils correctly."""
        mock_smirks_to_svg.return_value = "<svg>test_svg</svg>"

        rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")

        result = rule.as_svg
        mock_smirks_to_svg.assert_called_once_with(rule.smirks, True, width=800, height=400)
        self.assertEqual(result, "<svg>test_svg</svg>")

    def test_atomic_transaction(self):
        """Test that rule creation is atomic."""
        smirks = "[H:1][C:2]>>[H:1][O:2]"

        # This should work normally
        rule = SimpleAmbitRule.create(package=self.package, smirks=smirks)
        self.assertIsInstance(rule, SimpleAmbitRule)

        # Test transaction rollback on error
        with patch("epdb.models.SimpleAmbitRule.save", side_effect=Exception("Database error")):
            with self.assertRaises(Exception):
                SimpleAmbitRule.create(package=self.package, smirks="[H:3][C:4]>>[H:3][O:4]")

        # Verify no partial data was saved
        self.assertEqual(SimpleAmbitRule.objects.filter(package=self.package).count(), 1)

    def test_multiple_duplicate_warning(self):
        """Test logging when multiple duplicates are found."""
        smirks = "[H:1][C:2]>>[H:1][O:2]"

        # Create first rule
        rule1 = SimpleAmbitRule.create(package=self.package, smirks=smirks)

        # Manually create a duplicate to simulate the error condition
        rule2 = SimpleAmbitRule(package=self.package, smirks=smirks, name="Manual Rule")
        rule2.save()

        with patch("epdb.models.logger") as mock_logger:
            # This should find the existing rule and log an error about multiple matches
            result = SimpleAmbitRule.create(package=self.package, smirks=smirks)

            # Should return the first matching rule
            self.assertEqual(result.pk, rule1.pk)

            # Should log an error about multiple matches
            mock_logger.error.assert_called()
            self.assertIn("More than one rule matched", mock_logger.error.call_args[0][0])

    def test_model_fields(self):
        """Test model field properties."""
        rule = SimpleAmbitRule.create(
            package=self.package,
            smirks="[H:1][C:2]>>[H:1][O:2]",
            reactant_filter_smarts="[CH3]",
            product_filter_smarts="[OH]",
        )

        # Test field properties
        self.assertFalse(rule._meta.get_field("smirks").blank)
        self.assertFalse(rule._meta.get_field("smirks").null)
        self.assertTrue(rule._meta.get_field("reactant_filter_smarts").null)
        self.assertTrue(rule._meta.get_field("product_filter_smarts").null)

        # Test verbose names
        self.assertEqual(rule._meta.get_field("smirks").verbose_name, "SMIRKS")
        self.assertEqual(
            rule._meta.get_field("reactant_filter_smarts").verbose_name, "Reactant Filter SMARTS"
        )
        self.assertEqual(
            rule._meta.get_field("product_filter_smarts").verbose_name, "Product Filter SMARTS"
        )

    def test_reactant_filter_smarts(self):
        rule = SimpleAmbitRule.create(
            package=self.package,
            smirks="[#6,#8,#16:6]-[#6:4](=[O:5])[#6:2]=[#6;!$(CO)!$(C=CN[H])!$(C(=C)N[H]):1]>>[#8]([H])-[#6:1]-[#6:2]-[#6:4](-[#6,#8,#16:6])=[O:5]",
            reactant_filter_smarts="[$(OC(=O)C=CC=CC(O)=O),$(OC1C=CC=CC1O)]",
            product_filter_smarts=None,
        )

        SMILES = "OC(=O)C=CC=CC(O)=O"
        res = FormatConverter.apply(SMILES, rule.smirks)

        # Assert Rule triggers, if we use FormatConverter without reactant_filter_smarts passed along
        self.assertTrue(len(res) > 0)

        res = rule.apply(SMILES)

        # Check if reactant_filter_smarts prevents rule from application
        self.assertTrue(len(res) == 0)

    def test_product_filter_smarts(self):
        rule = SimpleAmbitRule.create(
            package=self.package,
            smirks="[#6,#8,#16:6]-[#6:4](=[O:5])[#6:2]=[#6;!$(CO)!$(C=CN[H])!$(C(=C)N[H]):1]>>[#8]([H])-[#6:1]-[#6:2]-[#6:4](-[#6,#8,#16:6])=[O:5]",
            reactant_filter_smarts=None,
            product_filter_smarts="[$(O=C(O)C=CC=CC(O)CC(=O)O)]",
        )

        SMILES = "OC(=O)C=CC=CC=CC(O)=O"
        res = FormatConverter.apply(SMILES, rule.smirks)

        # Assert Rule triggers, if we use FormatConverter without product_filter_smarts passed along
        self.assertTrue(len(res) > 0)

        res = rule.apply(SMILES)

        # Check if reactant_filter_smarts prevents rule from application
        self.assertTrue(len(res) == 0)
