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


from epdb.logic import PackageManager
from epdb.models import Compound, User, Reaction, Rule, SimpleAmbitRule, ParallelRule


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

    @classmethod
    def setUpClass(cls):
        super(CopyTest, cls).setUpClass()
        cls.user = User.objects.get(username="anonymous")
        cls.package = PackageManager.create_package(cls.user, "Source Package", "No Desc")
        cls.AFOXOLANER = Compound.create(
            cls.package,
            smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
            name="Afoxolaner",
            description="Test compound for copying",
        )

        cls.FOUR_NITROBENZOIC_ACID = Compound.create(
            cls.package,
            smiles="[O-][N+](=O)c1ccc(C(=O)[O-])cc1",  # Normalized: O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
            name="Test Compound",
            description="Compound with multiple structures",
        )

        cls.ETHANOL = Compound.create(
            cls.package, smiles="CCO", name="Ethanol", description="Simple alcohol"
        )
        cls.target_package = PackageManager.create_package(cls.user, "Target Package", "No Desc")

        cls.reaction_educt = Compound.create(
            cls.package,
            smiles="C(CCl)Cl",
            name="1,2-Dichloroethane",
            description="Eawag BBD compound c0001",
        ).default_structure

        cls.reaction_product = Compound.create(
            cls.package,
            smiles="C(CO)Cl",
            name="2-Chloroethanol",
            description="Eawag BBD compound c0005",
        ).default_structure

        cls.SIMPLE_RULE = Rule.create(
            rule_type="SimpleAmbitRule",
            package=cls.package,
            name="bt0022-2833",
            description="Dihalomethyl derivative + Halomethyl derivative > 1-Halo-1-methylalcohol derivative + 1-Methylalcohol derivative",
            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]",
        )

        cls.SIMPLE_RULE_2 = Rule.create(
            rule_type="SimpleAmbitRule",
            package=cls.package,
            name="Crap",
            description=None,
            smirks="CC>>CCC",
        )

        cls.PARALLEL_RULE = Rule.create(
            rule_type="ParallelRule",
            package=cls.package,
            simple_rules=[cls.SIMPLE_RULE, cls.SIMPLE_RULE_2],
            name="Par Rule",
            description=None,
            reactant_filter_smarts=None,
            product_filter_smarts=None,
        )

        cls.REACTION = Reaction.create(
            package=cls.package,
            name="Eawag BBD reaction r0001",
            educts=[cls.reaction_educt],
            products=[cls.reaction_product],
            rules=[cls.SIMPLE_RULE],
            multi_step=False,
        )

    def test_compound_copy_basic(self):
        """Test basic compound copying functionality"""
        mapping = dict()
        copied_compound = self.AFOXOLANER.copy(self.target_package, mapping)

        self.assertNotEqual(self.AFOXOLANER.uuid, copied_compound.uuid)
        self.assertEqual(self.AFOXOLANER.name, copied_compound.name)
        self.assertEqual(self.AFOXOLANER.description, copied_compound.description)
        self.assertEqual(copied_compound.package, self.target_package)
        self.assertEqual(self.AFOXOLANER.package, self.package)
        self.assertEqual(
            self.AFOXOLANER.default_structure.smiles, copied_compound.default_structure.smiles
        )

    def test_compound_copy_with_multiple_structures(self):
        """Test copying a compound with multiple structures"""

        original_structure_count = self.FOUR_NITROBENZOIC_ACID.structures.count()

        mapping = dict()
        # Copy the compound
        copied_compound = self.FOUR_NITROBENZOIC_ACID.copy(self.target_package, mapping)

        # Verify all structures were copied
        self.assertEqual(copied_compound.structures.count(), original_structure_count)

        # Verify default_structure is properly set
        self.assertIsNotNone(copied_compound.default_structure)
        self.assertEqual(
            copied_compound.default_structure.smiles,
            self.FOUR_NITROBENZOIC_ACID.default_structure.smiles,
        )

    def test_compound_copy_preserves_aliases(self):
        """Test that compound copying preserves aliases"""
        # Create a compound and add aliases
        original_compound = self.ETHANOL

        # Add aliases if the method exists
        if hasattr(original_compound, "add_alias"):
            original_compound.add_alias("Ethyl alcohol")
            original_compound.add_alias("Grain alcohol")

        mapping = dict()
        copied_compound = original_compound.copy(self.target_package, mapping)

        # Verify aliases were copied if they exist
        if hasattr(original_compound, "aliases") and hasattr(copied_compound, "aliases"):
            original_aliases = original_compound.aliases
            copied_aliases = copied_compound.aliases
            self.assertEqual(original_aliases, copied_aliases)

    def test_compound_copy_preserves_external_identifiers(self):
        """Test that external identifiers are preserved during copying"""
        original_compound = self.ETHANOL

        # Add external identifiers if the methods exist
        if hasattr(original_compound, "add_cas_number"):
            original_compound.add_cas_number("64-17-5")
        if hasattr(original_compound, "add_pubchem_compound_id"):
            original_compound.add_pubchem_compound_id("702")

        mapping = dict()
        copied_compound = original_compound.copy(self.target_package, mapping)

        # Verify external identifiers were copied
        original_ext_ids = original_compound.external_identifiers.all()
        copied_ext_ids = copied_compound.external_identifiers.all()

        self.assertEqual(original_ext_ids.count(), copied_ext_ids.count())

        # Check that identifier values match
        original_values = set(ext_id.identifier_value for ext_id in original_ext_ids)
        copied_values = set(ext_id.identifier_value for ext_id in copied_ext_ids)
        self.assertEqual(original_values, copied_values)

    def test_compound_copy_structure_properties(self):
        """Test that structure properties are properly copied"""
        original_compound = self.ETHANOL

        mapping = dict()
        copied_compound = original_compound.copy(self.target_package, mapping)

        # Verify structure properties
        original_structure = original_compound.default_structure
        copied_structure = copied_compound.default_structure

        self.assertEqual(original_structure.smiles, copied_structure.smiles)
        self.assertEqual(original_structure.canonical_smiles, copied_structure.canonical_smiles)
        self.assertEqual(original_structure.inchikey, copied_structure.inchikey)
        self.assertEqual(
            original_structure.normalized_structure, copied_structure.normalized_structure
        )

        # Verify they are different objects
        self.assertNotEqual(original_structure.uuid, copied_structure.uuid)
        self.assertEqual(copied_structure.compound, copied_compound)

    def test_reaction_copy_basic(self):
        """Test basic reaction copying functionality"""
        mapping = dict()
        copied_reaction = self.REACTION.copy(self.target_package, mapping)

        self.assertNotEqual(self.REACTION.uuid, copied_reaction.uuid)
        self.assertEqual(self.REACTION.name, copied_reaction.name)
        self.assertEqual(self.REACTION.description, copied_reaction.description)
        self.assertEqual(self.REACTION.multi_step, copied_reaction.multi_step)
        self.assertEqual(copied_reaction.package, self.target_package)
        self.assertEqual(self.REACTION.package, self.package)

    def test_reaction_copy_structures(self):
        """Test basic reaction copying functionality"""
        mapping = dict()
        copied_reaction = self.REACTION.copy(self.target_package, mapping)

        for orig_educt, copy_educt in zip(self.REACTION.educts.all(), copied_reaction.educts.all()):
            self.assertNotEqual(orig_educt.uuid, copy_educt.uuid)
            self.assertEqual(orig_educt.name, copy_educt.name)
            self.assertEqual(orig_educt.description, copy_educt.description)
            self.assertEqual(copy_educt.compound.package, self.target_package)
            self.assertEqual(orig_educt.compound.package, self.package)
            self.assertEqual(orig_educt.smiles, copy_educt.smiles)

        for orig_product, copy_product in zip(
            self.REACTION.products.all(), copied_reaction.products.all()
        ):
            self.assertNotEqual(orig_product.uuid, copy_product.uuid)
            self.assertEqual(orig_product.name, copy_product.name)
            self.assertEqual(orig_product.description, copy_product.description)
            self.assertEqual(copy_product.compound.package, self.target_package)
            self.assertEqual(orig_product.compound.package, self.package)
            self.assertEqual(orig_product.smiles, copy_product.smiles)

    def test_copy_compound_deduplication(self):
        mapping = dict()
        first_copy = self.AFOXOLANER.copy(self.target_package, mapping)

        # If we copy it again, we should get the exact same mapping and the number of
        # Compounds should not increase
        second_mapping = dict()
        second_copy = self.AFOXOLANER.copy(self.target_package, second_mapping)

        self.assertEqual(self.target_package.compounds.count(), 1)
        self.assertEqual(first_copy, second_copy)

        # Create a Compound where the initial SMILES is already normalized
        # The Compound will only have a CompoundStructure
        c = Compound.create(
            package=self.target_package,
            smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
            name="Compound with single structure",
            description="Compound with single structure",
        )

        self.assertEqual(c.structures.count(), 1)

        # Now we copy a Compound that share the same normalized structure but has
        # a non normalized. We expect them to be merged
        third_mapping = dict()
        third_copy = self.FOUR_NITROBENZOIC_ACID.copy(self.target_package, third_mapping)

        self.assertEqual(third_copy, c)
        self.assertEqual(c.structures.count(), 2)

    def test_copy_rule_deduplication(self):
        mapping = dict()
        first_copy = self.SIMPLE_RULE.copy(self.target_package, mapping)

        # If we copy it again, we should get the exact same mapping and the number of
        # Rule should not increase
        second_mapping = dict()
        second_copy = self.SIMPLE_RULE.copy(self.target_package, second_mapping)

        self.assertEqual(self.target_package.rules.count(), 1)
        self.assertEqual(first_copy, second_copy)

        third_mapping = dict()
        first_par_copy = self.PARALLEL_RULE.copy(self.target_package, third_mapping)
        # 1 ParallelRule, 2 SimpleRules
        self.assertEqual(self.target_package.rules.count(), 3)
        self.assertEqual(SimpleAmbitRule.objects.filter(package=self.target_package).count(), 2)
        self.assertEqual(ParallelRule.objects.filter(package=self.target_package).count(), 1)

        fourth_mapping = dict()
        second_par_copy = self.PARALLEL_RULE.copy(self.target_package, fourth_mapping)

        # Counts should remain...
        self.assertEqual(self.target_package.rules.count(), 3)
        self.assertEqual(SimpleAmbitRule.objects.filter(package=self.target_package).count(), 2)
        self.assertEqual(ParallelRule.objects.filter(package=self.target_package).count(), 1)

        # Mapping should be identical
        self.assertEqual(first_par_copy, second_par_copy)

    def test_copy_reaction_deduplication(self):
        mapping = dict()
        first_copy = self.REACTION.copy(self.target_package, mapping)

        # If we copy it again, we should get the exact same mapping and the number of
        # Reaction should not increase
        second_mapping = dict()
        second_copy = self.REACTION.copy(self.target_package, second_mapping)

        self.assertEqual(self.target_package.reactions.count(), 1)
        self.assertEqual(first_copy, second_copy)
