"""Contract tests for IUCLID XML builders - no DB required."""

import xml.etree.ElementTree as ET
from uuid import uuid4

from django.test import SimpleTestCase, tag

from epiuclid.builders.base import (
    NS_PLATFORM_CONTAINER,
    NS_PLATFORM_FIELDS,
    NS_PLATFORM_METADATA,
    document_key,
)
from epiuclid.builders.endpoint_study import DOC_SUBTYPE, EndpointStudyRecordBuilder, NS_ESR_BIODEG
from epiuclid.builders.reference_substance import NS_REFERENCE_SUBSTANCE, ReferenceSubstanceBuilder
from epiuclid.builders.substance import NS_SUBSTANCE, SubstanceBuilder

from .factories import (
    make_endpoint_study_record_data,
    make_half_life_entry,
    make_reference_substance_data,
    make_soil_properties_data,
    make_substance_data,
    make_transformation_entry,
)
from .xml_assertions import assert_xpath_absent, assert_xpath_text


@tag("iuclid")
class SubstanceBuilderContractTest(SimpleTestCase):
    def test_maps_name_and_reference_key(self):
        reference_uuid = uuid4()
        data = make_substance_data(name="Atrazine", reference_substance_uuid=reference_uuid)

        root = ET.fromstring(SubstanceBuilder().build(data))

        assert_xpath_text(self, root, f".//{{{NS_SUBSTANCE}}}ChemicalName", "Atrazine")
        assert_xpath_text(
            self,
            root,
            f".//{{{NS_SUBSTANCE}}}ReferenceSubstance/{{{NS_SUBSTANCE}}}ReferenceSubstance",
            document_key(reference_uuid),
        )

    def test_omits_reference_substance_when_missing(self):
        data = make_substance_data(reference_substance_uuid=None)

        root = ET.fromstring(SubstanceBuilder().build(data))

        assert_xpath_absent(self, root, f".//{{{NS_SUBSTANCE}}}ReferenceSubstance")

    def test_sets_substance_document_type(self):
        data = make_substance_data()

        root = ET.fromstring(SubstanceBuilder().build(data))

        assert_xpath_text(
            self,
            root,
            f"{{{NS_PLATFORM_CONTAINER}}}PlatformMetadata/{{{NS_PLATFORM_METADATA}}}documentType",
            "SUBSTANCE",
        )


@tag("iuclid")
class ReferenceSubstanceBuilderContractTest(SimpleTestCase):
    def test_maps_structural_identifiers_and_mass_precision(self):
        data = make_reference_substance_data(molecular_weight=215.6)

        root = ET.fromstring(ReferenceSubstanceBuilder().build(data))

        assert_xpath_text(
            self,
            root,
            f".//{{{NS_REFERENCE_SUBSTANCE}}}Inventory/{{{NS_REFERENCE_SUBSTANCE}}}CASNumber",
            "1912-24-9",
        )
        assert_xpath_text(
            self,
            root,
            f".//{{{NS_REFERENCE_SUBSTANCE}}}MolecularStructuralInfo/{{{NS_REFERENCE_SUBSTANCE}}}InChl",
            (
                "InChI=1S/C8H14ClN5/c1-4-10-7-12-6(9)11-8(13-7)"
                "14-5(2)3/h5H,4H2,1-3H3,(H2,10,11,12,13,14)"
            ),
        )
        assert_xpath_text(
            self,
            root,
            f".//{{{NS_REFERENCE_SUBSTANCE}}}MolecularStructuralInfo/{{{NS_REFERENCE_SUBSTANCE}}}InChIKey",
            "MXWJVTOOROXGIU-UHFFFAOYSA-N",
        )
        assert_xpath_text(
            self,
            root,
            f".//{{{NS_REFERENCE_SUBSTANCE}}}MolecularStructuralInfo"
            f"/{{{NS_REFERENCE_SUBSTANCE}}}MolecularWeightRange"
            f"/{{{NS_REFERENCE_SUBSTANCE}}}lowerValue",
            "215.60",
        )

    def test_omits_inventory_and_weight_for_minimal_payload(self):
        data = make_reference_substance_data(
            cas_number=None,
            molecular_formula=None,
            molecular_weight=None,
            inchi=None,
            inchi_key=None,
            smiles="CC",
        )

        root = ET.fromstring(ReferenceSubstanceBuilder().build(data))

        assert_xpath_absent(self, root, f".//{{{NS_REFERENCE_SUBSTANCE}}}Inventory")
        assert_xpath_absent(
            self,
            root,
            f".//{{{NS_REFERENCE_SUBSTANCE}}}MolecularWeightRange",
        )
        assert_xpath_text(
            self,
            root,
            f".//{{{NS_REFERENCE_SUBSTANCE}}}MolecularStructuralInfo/{{{NS_REFERENCE_SUBSTANCE}}}SmilesNotation",
            "CC",
        )


@tag("iuclid")
class EndpointStudyRecordBuilderContractTest(SimpleTestCase):
    def test_sets_document_metadata_and_parent_link(self):
        substance_uuid = uuid4()
        data = make_endpoint_study_record_data(substance_uuid=substance_uuid)

        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))

        metadata_root = f"{{{NS_PLATFORM_CONTAINER}}}PlatformMetadata"
        assert_xpath_text(
            self,
            root,
            f"{metadata_root}/{{{NS_PLATFORM_METADATA}}}documentType",
            "ENDPOINT_STUDY_RECORD",
        )
        assert_xpath_text(
            self,
            root,
            f"{metadata_root}/{{{NS_PLATFORM_METADATA}}}documentSubType",
            DOC_SUBTYPE,
        )
        assert_xpath_text(
            self,
            root,
            f"{metadata_root}/{{{NS_PLATFORM_METADATA}}}parentDocumentKey",
            document_key(substance_uuid),
        )
        assert_xpath_text(
            self,
            root,
            f"{metadata_root}/{{{NS_PLATFORM_METADATA}}}orderInSectionNo",
            "1",
        )

    def test_esr_metadata_order_uses_stax_safe_layout(self):
        data = make_endpoint_study_record_data()

        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))
        metadata = root.find(f"{{{NS_PLATFORM_CONTAINER}}}PlatformMetadata")

        self.assertIsNotNone(metadata)
        assert metadata is not None
        child_tags = [el.tag.split("}", 1)[1] for el in list(metadata)]
        self.assertEqual(
            child_tags,
            [
                "iuclidVersion",
                "documentKey",
                "documentType",
                "definitionVersion",
                "creationDate",
                "lastModificationDate",
                "name",
                "documentSubType",
                "parentDocumentKey",
                "orderInSectionNo",
                "i5Origin",
                "creationTool",
            ],
        )

    def test_omits_results_for_skeleton_payload(self):
        data = make_endpoint_study_record_data()

        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))

        assert_xpath_absent(self, root, f".//{{{NS_ESR_BIODEG}}}ResultsAndDiscussion")

    def test_maps_half_life_and_temperature_ranges(self):
        data = make_endpoint_study_record_data(
            half_lives=[make_half_life_entry(model="SFO", dt50_start=12.5, dt50_end=15.0)],
            temperature=(20.0, 20.0),
        )

        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))

        base = (
            f".//{{{NS_ESR_BIODEG}}}ResultsAndDiscussion"
            f"/{{{NS_ESR_BIODEG}}}DTParentCompound"
            f"/{{{NS_ESR_BIODEG}}}entry"
        )
        assert_xpath_text(self, root, f"{base}/{{{NS_ESR_BIODEG}}}KineticParameters", "SFO")
        assert_xpath_text(
            self, root, f"{base}/{{{NS_ESR_BIODEG}}}Value/{{{NS_ESR_BIODEG}}}lowerValue", "12.5"
        )
        assert_xpath_text(
            self, root, f"{base}/{{{NS_ESR_BIODEG}}}Value/{{{NS_ESR_BIODEG}}}upperValue", "15.0"
        )
        assert_xpath_text(
            self, root, f"{base}/{{{NS_ESR_BIODEG}}}Temp/{{{NS_ESR_BIODEG}}}lowerValue", "20.0"
        )
        assert_xpath_text(
            self, root, f"{base}/{{{NS_ESR_BIODEG}}}Temp/{{{NS_ESR_BIODEG}}}upperValue", "20.0"
        )

    def test_maps_soil_no_on_dt_entries(self):
        data = make_endpoint_study_record_data(
            half_lives=[
                make_half_life_entry(
                    model="SFO",
                    dt50_start=12.5,
                    dt50_end=15.0,
                    soil_no_code="2",
                    temperature=(22.0, 22.0),
                )
            ],
            temperature=None,
        )

        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))
        base = (
            f".//{{{NS_ESR_BIODEG}}}ResultsAndDiscussion"
            f"/{{{NS_ESR_BIODEG}}}DTParentCompound"
            f"/{{{NS_ESR_BIODEG}}}entry"
        )
        assert_xpath_text(
            self, root, f"{base}/{{{NS_ESR_BIODEG}}}SoilNo/{{{NS_ESR_BIODEG}}}value", "2"
        )
        assert_xpath_text(
            self, root, f"{base}/{{{NS_ESR_BIODEG}}}Temp/{{{NS_ESR_BIODEG}}}lowerValue", "22.0"
        )

    def test_maps_transformation_entries_and_model_context(self):
        parent_ref_uuid = uuid4()
        product_ref_uuid = uuid4()
        data = make_endpoint_study_record_data(
            transformation_products=[
                make_transformation_entry(
                    parent_reference_uuids=[parent_ref_uuid],
                    product_reference_uuid=product_ref_uuid,
                    kinetic_formation_fraction=0.42,
                )
            ],
            model_name_and_version=["Test model 1.0"],
            software_name_and_version=["enviPath"],
            model_remarks=["Model UUID: 00000000-0000-0000-0000-000000000000"],
        )

        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))

        assert_xpath_text(
            self,
            root,
            f".//{{{NS_ESR_BIODEG}}}MaterialsAndMethods"
            f"/{{{NS_ESR_BIODEG}}}ModelAndSoftware"
            f"/{{{NS_ESR_BIODEG}}}ModelNameAndVersion",
            "Test model 1.0",
        )
        entry_base = (
            f".//{{{NS_ESR_BIODEG}}}ResultsAndDiscussion"
            f"/{{{NS_ESR_BIODEG}}}TransformationProductsDetails"
            f"/{{{NS_ESR_BIODEG}}}entry"
        )
        assert_xpath_text(
            self,
            root,
            f"{entry_base}/{{{NS_ESR_BIODEG}}}IdentityOfCompound",
            document_key(product_ref_uuid),
        )
        assert_xpath_text(
            self,
            root,
            f"{entry_base}/{{{NS_ESR_BIODEG}}}ParentCompoundS/{{{NS_PLATFORM_FIELDS}}}key",
            document_key(parent_ref_uuid),
        )
        assert_xpath_text(
            self,
            root,
            f"{entry_base}/{{{NS_ESR_BIODEG}}}KineticFormationFraction",
            "0.42",
        )

    def test_temperature_without_half_lives_in_xml(self):
        """Temperature with no half-lives still renders a DTParentCompound entry."""
        data = make_endpoint_study_record_data(temperature=(21.0, 21.0))
        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))
        base = (
            f".//{{{NS_ESR_BIODEG}}}ResultsAndDiscussion"
            f"/{{{NS_ESR_BIODEG}}}DTParentCompound"
            f"/{{{NS_ESR_BIODEG}}}entry"
        )
        assert_xpath_text(
            self, root, f"{base}/{{{NS_ESR_BIODEG}}}Temp/{{{NS_ESR_BIODEG}}}lowerValue", "21.0"
        )
        assert_xpath_text(
            self, root, f"{base}/{{{NS_ESR_BIODEG}}}Temp/{{{NS_ESR_BIODEG}}}upperValue", "21.0"
        )

    def test_temperature_interval_in_xml(self):
        """Temperature tuple renders as lowerValue/upperValue in Temp element."""
        hl = make_half_life_entry()
        data = make_endpoint_study_record_data(half_lives=[hl], temperature=(20.0, 25.0))
        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))
        base = (
            f".//{{{NS_ESR_BIODEG}}}ResultsAndDiscussion"
            f"/{{{NS_ESR_BIODEG}}}DTParentCompound"
            f"/{{{NS_ESR_BIODEG}}}entry"
        )
        assert_xpath_text(
            self, root, f"{base}/{{{NS_ESR_BIODEG}}}Temp/{{{NS_ESR_BIODEG}}}lowerValue", "20.0"
        )
        assert_xpath_text(
            self, root, f"{base}/{{{NS_ESR_BIODEG}}}Temp/{{{NS_ESR_BIODEG}}}upperValue", "25.0"
        )

    def test_esr_with_soil_properties_emits_structured_soil_by_default(self):
        props = make_soil_properties_data(clay=15.0, silt=35.0, sand=50.0)
        data = make_endpoint_study_record_data(soil_properties=props)
        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))

        entry_path = (
            f".//{{{NS_ESR_BIODEG}}}StudyDesign"
            f"/{{{NS_ESR_BIODEG}}}SoilProperties"
            f"/{{{NS_ESR_BIODEG}}}entry"
        )
        assert_xpath_text(
            self,
            root,
            f"{entry_path}/{{{NS_ESR_BIODEG}}}Clay/{{{NS_ESR_BIODEG}}}lowerValue",
            "15.0",
        )
        assert_xpath_text(
            self,
            root,
            f"{entry_path}/{{{NS_ESR_BIODEG}}}Silt/{{{NS_ESR_BIODEG}}}lowerValue",
            "35.0",
        )
        assert_xpath_text(
            self,
            root,
            f"{entry_path}/{{{NS_ESR_BIODEG}}}Sand/{{{NS_ESR_BIODEG}}}lowerValue",
            "50.0",
        )
        assert_xpath_text(
            self,
            root,
            f".//{{{NS_ESR_BIODEG}}}StudyDesign"
            f"/{{{NS_ESR_BIODEG}}}SoilClassification"
            f"/{{{NS_ESR_BIODEG}}}value",
            "1649",
        )
        assert_xpath_absent(
            self,
            root,
            f".//{{{NS_ESR_BIODEG}}}StudyDesign"
            f"/{{{NS_ESR_BIODEG}}}SoilClassification"
            f"/{{{NS_ESR_BIODEG}}}other",
        )
        assert_xpath_text(
            self,
            root,
            f"{entry_path}/{{{NS_ESR_BIODEG}}}SoilType/{{{NS_ESR_BIODEG}}}value",
            "1026",
        )
        assert_xpath_absent(
            self,
            root,
            f"{entry_path}/{{{NS_ESR_BIODEG}}}SoilType/{{{NS_ESR_BIODEG}}}other",
        )
        assert_xpath_absent(
            self,
            root,
            f".//{{{NS_ESR_BIODEG}}}AnyOtherInformationOnMaterialsAndMethodsInclTables",
        )
        assert_xpath_absent(self, root, f".//{{{NS_ESR_BIODEG}}}DetailsOnSoilCharacteristics")

    def test_maps_multiple_soil_entries_with_soil_no(self):
        data = make_endpoint_study_record_data(
            soil_properties_entries=[
                make_soil_properties_data(soil_no_code="2", soil_type="LOAMY_SAND", sand=83.1),
                make_soil_properties_data(soil_no_code="4", soil_type="CLAY_LOAM", sand=23.7),
            ]
        )
        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))

        entries = root.findall(
            f".//{{{NS_ESR_BIODEG}}}StudyDesign"
            f"/{{{NS_ESR_BIODEG}}}SoilProperties"
            f"/{{{NS_ESR_BIODEG}}}entry"
        )
        self.assertEqual(len(entries), 2)

        soil_no_values = [
            entry.findtext(f"{{{NS_ESR_BIODEG}}}SoilNo/{{{NS_ESR_BIODEG}}}value")
            for entry in entries
        ]
        self.assertEqual(soil_no_values, ["2", "4"])

    def test_maps_soil_type_and_soil_classification_to_structured_fields(self):
        props = make_soil_properties_data(soil_type="LOAMY_SAND", soil_classification="USDA")
        data = make_endpoint_study_record_data(soil_properties=props)
        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))

        entry_path = (
            f".//{{{NS_ESR_BIODEG}}}StudyDesign"
            f"/{{{NS_ESR_BIODEG}}}SoilProperties"
            f"/{{{NS_ESR_BIODEG}}}entry"
        )
        assert_xpath_text(
            self,
            root,
            f"{entry_path}/{{{NS_ESR_BIODEG}}}SoilType/{{{NS_ESR_BIODEG}}}value",
            "1027",
        )
        assert_xpath_absent(
            self, root, f"{entry_path}/{{{NS_ESR_BIODEG}}}SoilType/{{{NS_ESR_BIODEG}}}other"
        )
        assert_xpath_text(
            self,
            root,
            f".//{{{NS_ESR_BIODEG}}}StudyDesign"
            f"/{{{NS_ESR_BIODEG}}}SoilClassification"
            f"/{{{NS_ESR_BIODEG}}}value",
            "1649",
        )
        assert_xpath_absent(
            self,
            root,
            f".//{{{NS_ESR_BIODEG}}}StudyDesign"
            f"/{{{NS_ESR_BIODEG}}}SoilClassification"
            f"/{{{NS_ESR_BIODEG}}}other",
        )

    def test_unknown_soil_type_and_classification_use_open_picklist(self):
        props = make_soil_properties_data(soil_type="SILTY_SAND", soil_classification="UK_ADAS")
        data = make_endpoint_study_record_data(soil_properties=props)
        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))

        entry_path = (
            f".//{{{NS_ESR_BIODEG}}}StudyDesign"
            f"/{{{NS_ESR_BIODEG}}}SoilProperties"
            f"/{{{NS_ESR_BIODEG}}}entry"
        )
        assert_xpath_text(
            self,
            root,
            f"{entry_path}/{{{NS_ESR_BIODEG}}}SoilType/{{{NS_ESR_BIODEG}}}value",
            "1342",
        )
        assert_xpath_text(
            self,
            root,
            f"{entry_path}/{{{NS_ESR_BIODEG}}}SoilType/{{{NS_ESR_BIODEG}}}other",
            "SILTY SAND",
        )
        assert_xpath_text(
            self,
            root,
            f".//{{{NS_ESR_BIODEG}}}StudyDesign"
            f"/{{{NS_ESR_BIODEG}}}SoilClassification"
            f"/{{{NS_ESR_BIODEG}}}value",
            "1342",
        )
        assert_xpath_text(
            self,
            root,
            f".//{{{NS_ESR_BIODEG}}}StudyDesign"
            f"/{{{NS_ESR_BIODEG}}}SoilClassification"
            f"/{{{NS_ESR_BIODEG}}}other",
            "UK ADAS",
        )

    def test_infers_usda_soil_classification_from_soil_type(self):
        props = make_soil_properties_data(soil_type="LOAMY_SAND", soil_classification=None)
        data = make_endpoint_study_record_data(soil_properties=props)
        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))

        assert_xpath_text(
            self,
            root,
            f".//{{{NS_ESR_BIODEG}}}StudyDesign"
            f"/{{{NS_ESR_BIODEG}}}SoilClassification"
            f"/{{{NS_ESR_BIODEG}}}value",
            "1649",
        )
        assert_xpath_absent(
            self,
            root,
            f".//{{{NS_ESR_BIODEG}}}StudyDesign/{{{NS_ESR_BIODEG}}}SoilClassification/{{{NS_ESR_BIODEG}}}other",
        )

    def test_esr_without_soil_properties_omits_study_design(self):
        """ESR with soil_properties=None → no <StudyDesign> in XML."""
        data = make_endpoint_study_record_data(soil_properties=None)
        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))
        assert_xpath_absent(self, root, f".//{{{NS_ESR_BIODEG}}}StudyDesign")

    def test_omits_empty_ph_measured_in(self):
        props = make_soil_properties_data(ph_method="")
        data = make_endpoint_study_record_data(soil_properties=props)

        root = ET.fromstring(EndpointStudyRecordBuilder().build(data))

        self.assertNotIn("PHMeasuredIn", ET.tostring(root, encoding="unicode"))
