"""Offline DPD DE ShipmentService 3.2 weight exercise; never sends a shipment."""
from decimal import Decimal
from hashlib import sha256
import json
from pathlib import Path
import sys

from lxml import etree

SCHEMA_SHA256 = "d33b6e03eb0b9c46af9c9e31f9df7364be49598e7c175e0fad1bff49cfa440fb"
NAMESPACE = "http://dpd.com/common/service/types/ShipmentService/3.2"
checks = []
connections = []


def deny_connection(event, args):
    if event == "socket.connect":
        connections.append(event)
        raise RuntimeError("This exercise never connects to DPD")


sys.addaudithook(deny_connection)


def check(name, condition):
    if name in checks or not condition:
        raise AssertionError(name)
    checks.append(name)


def units_from_kg(value):
    """Local policy: accept exact positive multiples of 10g, otherwise review."""
    if not isinstance(value, str):
        raise ValueError("A decimal string with an explicit kg unit is required")
    mass = Decimal(value)
    if not mass.is_finite() or mass <= 0:
        raise ValueError("Review the mass before generating the request")
    units = mass * 100
    if units != units.to_integral_value():
        raise ValueError("No rounding direction has been approved")
    return int(units)


def request(total, parcels):
    root = etree.Element("{%s}storeOrders" % NAMESPACE, nsmap={"dpd": NAMESPACE})
    order = etree.SubElement(root, "order")
    general = etree.SubElement(order, "generalShipmentData")
    for key, value in [("sendingDepot", "0163"), ("product", "CL")]:
        etree.SubElement(general, key).text = value
    if total is not None:
        etree.SubElement(general, "mpsWeight").text = str(total)
    for role in ["sender", "recipient"]:
        address = etree.SubElement(general, role)
        for key, value in [("name1", "Fixture only"), ("street", "Teststrasse"),
                           ("country", "DE"), ("zipCode", "10115"), ("city", "Berlin")]:
            etree.SubElement(address, key).text = value
    for reference, weight in parcels:
        parcel = etree.SubElement(order, "parcels")
        etree.SubElement(parcel, "customerReferenceNumber1").text = reference
        if weight is not None:
            etree.SubElement(parcel, "weight").text = str(weight)
    service = etree.SubElement(order, "productAndServiceData")
    etree.SubElement(service, "orderType").text = "consignment"
    return root


def manifest_review(root, approved_kg):
    """A local pre-send check against a separately approved packed-parcel manifest."""
    expected = {reference: units_from_kg(kg) for reference, kg in approved_kg.items()}
    parcels = root.findall("order/parcels")
    references = [p.findtext("customerReferenceNumber1") for p in parcels]
    if len(references) != len(set(references)):
        return "duplicate_parcel_reference"
    if set(references) != set(expected):
        return "parcel_set_mismatch"
    for parcel, reference in zip(parcels, references):
        raw = parcel.findtext("weight")
        if raw is None or int(raw) != expected[reference]:
            return "parcel_mass_mismatch"
    raw_total = root.findtext("order/generalShipmentData/mpsWeight")
    if raw_total is None or int(raw_total) != sum(expected.values()):
        return "shipment_mass_mismatch"
    return "ready_for_pre_send_review"


def main(schema_path):
    raw = Path(schema_path).read_bytes()
    check("unmodified_provider_schema", sha256(raw).hexdigest() == SCHEMA_SHA256)
    parser = etree.XMLParser(no_network=True, resolve_entities=False)
    schema_document = etree.fromstring(raw, parser)
    schema = etree.XMLSchema(schema_document)
    check("provider_schema_compiled", schema is not None)
    xs = {"xs": "http://www.w3.org/2001/XMLSchema"}
    for parent, name, base in [("parcel", "weight", "xs:int"),
                               ("generalShipmentData", "mpsWeight", "xs:long")]:
        path = '//xs:complexType[@name="%s"]/xs:sequence/xs:element[@name="%s"]' % (parent, name)
        element = schema_document.xpath(path, namespaces=xs)[0]
        restriction = element.find("xs:simpleType/xs:restriction", xs)
        check(name + "_native_base", restriction.get("base") == base)
        check(name + "_native_optional", element.get("minOccurs") == "0")
        check(name + "_native_max", restriction.find("xs:maxInclusive", xs).get("value") == "99999999")
    check("1250g_exact_conversion", units_from_kg("1.25") == 125)
    check("documented_3kg_conversion", units_from_kg("3") == 300)
    for index, value in enumerate(["1.251", "0", "-1.25", "NaN", "Infinity", 1.25]):
        try:
            units_from_kg(value)
        except ValueError:
            check("local_mass_review_%s" % index, True)
        else:
            raise AssertionError("Unexpected automatic mass conversion")
    one = {"P-01": "1.25"}
    two = {"P-01": "2", "P-02": "2.25"}
    cases = [
        ("correct_single", 125, [("P-01", 125)], one, True, "ready_for_pre_send_review"),
        ("grams_in_wire_field", 1250, [("P-01", 1250)], one, True, "parcel_mass_mismatch"),
        ("decimal_in_integer_field", 125, [("P-01", "1.25")], one, False, None),
        ("comma_in_integer_field", 125, [("P-01", "1,25")], one, False, None),
        ("correct_two_parcels", 425, [("P-01", 200), ("P-02", 225)], two, True, "ready_for_pre_send_review"),
        ("shipment_total_on_each", 425, [("P-01", 425), ("P-02", 425)], two, True, "parcel_mass_mismatch"),
        ("swapped_masses_same_total", 425, [("P-01", 225), ("P-02", 200)], two, True, "parcel_mass_mismatch"),
        ("reordered_parcels", 425, [("P-02", 225), ("P-01", 200)], two, True, "ready_for_pre_send_review"),
        ("missing_parcel", 425, [("P-01", 200)], two, True, "parcel_set_mismatch"),
        ("duplicate_reference", 425, [("P-01", 200), ("P-01", 225)], two, True, "duplicate_parcel_reference"),
        ("optional_parcel_weight_omitted", 125, [("P-01", None)], one, True, "parcel_mass_mismatch"),
        ("optional_total_omitted", None, [("P-01", 125)], one, True, "shipment_mass_mismatch"),
        ("wrong_total", 400, [("P-01", 200), ("P-02", 225)], two, True, "shipment_mass_mismatch"),
        ("negative_native_integer", -125, [("P-01", -125)], one, True, "parcel_mass_mismatch"),
        ("above_native_max", 100000000, [("P-01", 100000000)], one, False, None),
    ]
    observations = []
    for name, total, parcels, manifest, native_expected, local_expected in cases:
        root = request(total, parcels)
        native_valid = schema.validate(root)
        check(name + "_native_xsd", native_valid == native_expected)
        local = manifest_review(root, manifest) if native_valid else "schema_rejected"
        check(name + "_local_decision", local == (local_expected or "schema_rejected"))
        observations.append({"case": name, "nativeXsdValid": native_valid, "localDecision": local})
    check("no_network_connections", not connections)
    print(json.dumps({"schemaSha256": SCHEMA_SHA256, "lxml": ".".join(map(str, etree.LXML_VERSION[:3])),
                      "checksPassed": len(checks), "nativeSchemaRuns": len(cases),
                      "networkConnectionAttempts": len(connections), "externalWrites": 0,
                      "observations": observations}, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main(sys.argv[1])
