"""
Unit and integration tests for transform map import/export utilities.
"""
import json

from django.contrib.contenttypes.models import ContentType
from django.test import TestCase

from ipfabric_netbox.models import IPFabricEndpoint
from ipfabric_netbox.models import IPFabricRelationshipField
from ipfabric_netbox.models import IPFabricTransformField
from ipfabric_netbox.models import IPFabricTransformMap
from ipfabric_netbox.models import IPFabricTransformMapGroup
from ipfabric_netbox.utilities.transform_map import _normalize_parent_spec
from ipfabric_netbox.utilities.transform_map import _resolve_parent
from ipfabric_netbox.utilities.transform_map import build_transform_maps
from ipfabric_netbox.utilities.transform_map import export_transform_maps


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _site_ep():
    return IPFabricEndpoint.objects.get(endpoint="/inventory/sites/overview")


def _device_ep():
    return IPFabricEndpoint.objects.get(endpoint="/inventory/devices")


def _ct(app_label, model):
    return ContentType.objects.get(app_label=app_label, model=model)


def _payload(
    name,
    endpoint_str,
    target_app,
    target_model,
    group=None,
    parents=None,
    field_maps=None,
    relationship_maps=None,
):
    return {
        "data": {
            "name": name,
            "source_endpoint": endpoint_str,
            "target_model": {"app_label": target_app, "model": target_model},
            "group": group,
            "parents": parents,
        },
        "field_maps": field_maps or [],
        "relationship_maps": relationship_maps or [],
    }


# ---------------------------------------------------------------------------
# _normalize_parent_spec
# ---------------------------------------------------------------------------


class NormalizeParentSpecTests(TestCase):
    def test_string_becomes_ungrouped_dict(self):
        self.assertEqual(
            _normalize_parent_spec("dcim.site"),
            {"group": None, "target_model": "dcim.site"},
        )

    def test_dict_with_group(self):
        spec = {"group": "Prod", "target_model": "dcim.device"}
        self.assertEqual(_normalize_parent_spec(spec), spec)

    def test_dict_null_group(self):
        spec = {"group": None, "target_model": "ipam.vrf"}
        self.assertEqual(_normalize_parent_spec(spec), spec)

    def test_dict_missing_group_defaults_none(self):
        self.assertEqual(
            _normalize_parent_spec({"target_model": "dcim.site"}),
            {"group": None, "target_model": "dcim.site"},
        )


# ---------------------------------------------------------------------------
# _resolve_parent
# ---------------------------------------------------------------------------


class ResolveParentTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.group_a = IPFabricTransformMapGroup.objects.create(name="Resolve Group A")
        cls.site_ep = _site_ep()
        cls.device_ep = _device_ep()
        cls.site_ct = _ct("dcim", "site")
        cls.device_ct = _ct("dcim", "device")

        # Reuse the ungrouped site map already created by the migration
        # rather than creating a duplicate (which would cause MultipleObjectsReturned).
        cls.site_ungrouped = IPFabricTransformMap.objects.get(
            target_model=cls.site_ct,
            group=None,
        )
        cls.site_in_a = IPFabricTransformMap.objects.create(
            name="Site In A",
            source_endpoint=cls.site_ep,
            target_model=cls.site_ct,
            group=cls.group_a,
        )

    def test_resolve_ungrouped(self):
        result = _resolve_parent(
            {"group": None, "target_model": "dcim.site"},
            ContentType,
            IPFabricTransformMap,
            "default",
        )
        self.assertEqual(result, self.site_ungrouped)

    def test_resolve_named_group(self):
        result = _resolve_parent(
            {"group": "Resolve Group A", "target_model": "dcim.site"},
            ContentType,
            IPFabricTransformMap,
            "default",
        )
        self.assertEqual(result, self.site_in_a)

    def test_not_found_raises_value_error(self):
        with self.assertRaises(ValueError) as ctx:
            _resolve_parent(
                {"group": "Missing Group", "target_model": "dcim.site"},
                ContentType,
                IPFabricTransformMap,
                "default",
            )
        self.assertIn("not found", str(ctx.exception))

    def test_bad_content_type_raises(self):
        with self.assertRaises(ValueError) as ctx:
            _resolve_parent(
                {"group": None, "target_model": "dcim.doesnotexist"},
                ContentType,
                IPFabricTransformMap,
                "default",
            )
        self.assertIn("ContentType", str(ctx.exception))

    def test_multiple_objects_raises(self):
        # Force duplicate ungrouped device maps (bypass clean())
        IPFabricTransformMap(
            name="Dev Dup 1",
            source_endpoint=self.device_ep,
            target_model=self.device_ct,
            group=None,
        ).save()
        IPFabricTransformMap(
            name="Dev Dup 2",
            source_endpoint=self.device_ep,
            target_model=self.device_ct,
            group=None,
        ).save()
        with self.assertRaises(ValueError) as ctx:
            _resolve_parent(
                {"group": None, "target_model": "dcim.device"},
                ContentType,
                IPFabricTransformMap,
                "default",
            )
        self.assertIn("Multiple", str(ctx.exception))


# ---------------------------------------------------------------------------
# build_transform_maps – basic
# ---------------------------------------------------------------------------


class BuildTransformMapsBasicTests(TestCase):
    def setUp(self):
        self.group = IPFabricTransformMapGroup.objects.create(name="Build Basic Group")

    def test_creates_map(self):
        build_transform_maps(
            [
                _payload(
                    "Basic Site",
                    "/inventory/sites/overview",
                    "dcim",
                    "site",
                    group="Build Basic Group",
                )
            ]
        )
        self.assertTrue(
            IPFabricTransformMap.objects.filter(
                name="Basic Site", group=self.group
            ).exists()
        )

    def test_creates_field_maps(self):
        build_transform_maps(
            [
                _payload(
                    "FieldMap Site",
                    "/inventory/sites/overview",
                    "dcim",
                    "site",
                    group="Build Basic Group",
                    field_maps=[
                        {
                            "source_field": "siteName",
                            "target_field": "name",
                            "coalesce": False,
                            "template": "",
                        },
                        {
                            "source_field": "siteName",
                            "target_field": "slug",
                            "coalesce": True,
                            "template": "{{ object.siteName | slugify }}",
                        },
                    ],
                )
            ]
        )
        tm = IPFabricTransformMap.objects.get(name="FieldMap Site")
        self.assertEqual(tm.field_maps.count(), 2)

    def test_creates_relationship_maps(self):
        build_transform_maps(
            [
                _payload(
                    "RelMap Device",
                    "/inventory/devices",
                    "dcim",
                    "device",
                    group="Build Basic Group",
                    relationship_maps=[
                        {
                            "source_model": {"app_label": "dcim", "model": "site"},
                            "target_field": "site",
                            "coalesce": True,
                            "template": "{{ object.siteName }}",
                        }
                    ],
                )
            ]
        )
        tm = IPFabricTransformMap.objects.get(name="RelMap Device")
        self.assertEqual(tm.relationship_maps.count(), 1)
        self.assertEqual(tm.relationship_maps.first().target_field, "site")

    def test_strips_meta_fields(self):
        data = [
            {
                "data": {
                    "name": "Meta Strip",
                    "source_endpoint": "/inventory/sites/overview",
                    "target_model": {"app_label": "dcim", "model": "site"},
                    "group": "Build Basic Group",
                    "parents": None,
                    "created": "2024-01-01T00:00:00Z",
                    "last_updated": "2024-06-01T00:00:00Z",
                    "custom_field_data": {"cf": "val"},
                },
                "field_maps": [],
                "relationship_maps": [],
            }
        ]
        build_transform_maps(data)
        self.assertTrue(IPFabricTransformMap.objects.filter(name="Meta Strip").exists())

    def test_group_auto_created(self):
        build_transform_maps(
            [
                _payload(
                    "AutoGroup Map",
                    "/inventory/sites/overview",
                    "dcim",
                    "site",
                    group="AutoCreated Group XYZ",
                )
            ]
        )
        self.assertTrue(
            IPFabricTransformMapGroup.objects.filter(
                name="AutoCreated Group XYZ"
            ).exists()
        )

    def test_null_group_is_ungrouped(self):
        build_transform_maps(
            [
                _payload(
                    "No Group Map",
                    "/inventory/sites/overview",
                    "dcim",
                    "site",
                    group=None,
                )
            ]
        )
        tm = IPFabricTransformMap.objects.get(name="No Group Map")
        self.assertIsNone(tm.group)

    def test_missing_endpoint_prefix(self):
        build_transform_maps(
            [_payload("Bad Endpoint", "/no/such/endpoint", "dcim", "site")]
        )
        tm = IPFabricTransformMap.objects.filter(
            name__startswith="[NEEDS CORRECTION"
        ).first()
        self.assertIsNotNone(tm)
        self.assertIn("/no/such/endpoint", tm.name)


# ---------------------------------------------------------------------------
# build_transform_maps – parents
# ---------------------------------------------------------------------------


class BuildTransformMapsParentTests(TestCase):
    def setUp(self):
        self.site_ep = _site_ep()
        self.device_ep = _device_ep()

    def test_legacy_string_parent(self):
        # Use the ungrouped site map already created by the migration.
        site = IPFabricTransformMap.objects.get(
            target_model=_ct("dcim", "site"),
            group=None,
        )
        build_transform_maps(
            [
                _payload(
                    "Legacy Child",
                    "/inventory/devices",
                    "dcim",
                    "device",
                    group=None,
                    parents="dcim.site",
                )
            ]
        )
        child = IPFabricTransformMap.objects.get(name="Legacy Child")
        self.assertIn(site, child.parents.all())

    def test_legacy_list_of_strings(self):
        # Use the ungrouped site map already created by the migration.
        site = IPFabricTransformMap.objects.get(
            target_model=_ct("dcim", "site"),
            group=None,
        )
        build_transform_maps(
            [
                _payload(
                    "Legacy List Child",
                    "/inventory/devices",
                    "dcim",
                    "device",
                    group=None,
                    parents=["dcim.site"],
                )
            ]
        )
        child = IPFabricTransformMap.objects.get(name="Legacy List Child")
        self.assertIn(site, child.parents.all())

    def test_two_pass_child_before_parent(self):
        """Child before parent in JSON is still correctly wired after pass 2."""
        IPFabricTransformMapGroup.objects.create(name="TwoPass Group")
        data = [
            _payload(
                "TwoPass Child",
                "/inventory/devices",
                "dcim",
                "device",
                group="TwoPass Group",
                parents=[{"group": "TwoPass Group", "target_model": "dcim.site"}],
            ),
            _payload(
                "TwoPass Parent",
                "/inventory/sites/overview",
                "dcim",
                "site",
                group="TwoPass Group",
            ),
        ]
        build_transform_maps(data)
        child = IPFabricTransformMap.objects.get(name="TwoPass Child")
        parent = IPFabricTransformMap.objects.get(name="TwoPass Parent")
        self.assertIn(parent, child.parents.all())

    def test_cross_group_parent(self):
        grp_a = IPFabricTransformMapGroup.objects.create(name="Cross A")
        IPFabricTransformMapGroup.objects.create(name="Cross B")
        parent = IPFabricTransformMap.objects.create(
            name="Cross Parent",
            source_endpoint=self.site_ep,
            target_model=_ct("dcim", "site"),
            group=grp_a,
        )
        build_transform_maps(
            [
                _payload(
                    "Cross Child",
                    "/inventory/devices",
                    "dcim",
                    "device",
                    group="Cross B",
                    parents=[{"group": "Cross A", "target_model": "dcim.site"}],
                )
            ]
        )
        child = IPFabricTransformMap.objects.get(name="Cross Child")
        self.assertIn(parent, child.parents.all())

    def test_cross_group_missing_raises(self):
        with self.assertRaises(ValueError):
            build_transform_maps(
                [
                    _payload(
                        "Broken Child",
                        "/inventory/devices",
                        "dcim",
                        "device",
                        group=None,
                        parents=[
                            {"group": "No Such Group", "target_model": "dcim.site"}
                        ],
                    )
                ]
            )

    def test_multiple_parents(self):
        grp = IPFabricTransformMapGroup.objects.create(name="Multi Parent Group")
        site = IPFabricTransformMap.objects.create(
            name="MP Site",
            source_endpoint=self.site_ep,
            target_model=_ct("dcim", "site"),
            group=grp,
        )
        mfr = IPFabricTransformMap.objects.create(
            name="MP Manufacturer",
            source_endpoint=self.device_ep,
            target_model=_ct("dcim", "manufacturer"),
            group=grp,
        )
        build_transform_maps(
            [
                _payload(
                    "MP Device",
                    "/inventory/devices",
                    "dcim",
                    "device",
                    group="Multi Parent Group",
                    parents=[
                        {"group": "Multi Parent Group", "target_model": "dcim.site"},
                        {
                            "group": "Multi Parent Group",
                            "target_model": "dcim.manufacturer",
                        },
                    ],
                )
            ]
        )
        child = IPFabricTransformMap.objects.get(name="MP Device")
        self.assertEqual(child.parents.count(), 2)
        self.assertIn(site, child.parents.all())
        self.assertIn(mfr, child.parents.all())


# ---------------------------------------------------------------------------
# export_transform_maps
# ---------------------------------------------------------------------------


class ExportTransformMapsTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.group = IPFabricTransformMapGroup.objects.create(name="Export Test Group")
        cls.site_ep = _site_ep()
        cls.device_ep = _device_ep()
        cls.site_ct = _ct("dcim", "site")
        cls.device_ct = _ct("dcim", "device")

        cls.site_map = IPFabricTransformMap.objects.create(
            name="Exp Site Map",
            source_endpoint=cls.site_ep,
            target_model=cls.site_ct,
            group=cls.group,
        )
        IPFabricTransformField.objects.create(
            transform_map=cls.site_map,
            source_field="siteName",
            target_field="name",
            coalesce=False,
            template="",
        )
        IPFabricTransformField.objects.create(
            transform_map=cls.site_map,
            source_field="siteName",
            target_field="slug",
            coalesce=True,
            template="{{ object.siteName | slugify }}",
        )

        cls.device_map = IPFabricTransformMap.objects.create(
            name="Exp Device Map",
            source_endpoint=cls.device_ep,
            target_model=cls.device_ct,
            group=cls.group,
        )
        cls.device_map.parents.add(cls.site_map)
        IPFabricRelationshipField.objects.create(
            transform_map=cls.device_map,
            source_model=cls.site_ct,
            target_field="site",
            coalesce=True,
            template="{{ object.siteName }}",
        )

    def _export(self):
        return export_transform_maps(
            queryset=IPFabricTransformMap.objects.filter(group=self.group)
        )

    def test_count(self):
        self.assertEqual(len(self._export()), 2)

    def test_required_keys(self):
        for entry in self._export():
            self.assertIn("data", entry)
            self.assertIn("field_maps", entry)
            self.assertIn("relationship_maps", entry)

    def test_source_endpoint_is_path_string(self):
        for entry in self._export():
            ep = entry["data"]["source_endpoint"]
            self.assertIsInstance(ep, str)
            self.assertTrue(ep.startswith("/"))

    def test_target_model_is_dict(self):
        for entry in self._export():
            tm = entry["data"]["target_model"]
            self.assertIn("app_label", tm)
            self.assertIn("model", tm)

    def test_group_is_name(self):
        for entry in self._export():
            self.assertEqual(entry["data"]["group"], "Export Test Group")

    def test_null_group_is_none(self):
        ug = IPFabricTransformMap.objects.create(
            name="UG Export",
            source_endpoint=self.site_ep,
            target_model=self.site_ct,
            group=None,
        )
        result = export_transform_maps(
            queryset=IPFabricTransformMap.objects.filter(pk=ug.pk)
        )
        self.assertIsNone(result[0]["data"]["group"])
        ug.delete()

    def test_parents_are_dicts(self):
        result = self._export()
        dev = next(e for e in result if e["data"]["name"] == "Exp Device Map")
        parents = dev["data"]["parents"]
        self.assertIsNotNone(parents)
        self.assertEqual(len(parents), 1)
        p = parents[0]
        self.assertIn("group", p)
        self.assertIn("target_model", p)
        self.assertIn(".", p["target_model"])

    def test_no_parents_is_none(self):
        result = self._export()
        site = next(e for e in result if e["data"]["name"] == "Exp Site Map")
        self.assertIsNone(site["data"]["parents"])

    def test_field_maps_structure(self):
        result = self._export()
        site = next(e for e in result if e["data"]["name"] == "Exp Site Map")
        self.assertEqual(len(site["field_maps"]), 2)
        for fm in site["field_maps"]:
            for key in ("source_field", "target_field", "coalesce", "template"):
                self.assertIn(key, fm)

    def test_relationship_maps_structure(self):
        result = self._export()
        dev = next(e for e in result if e["data"]["name"] == "Exp Device Map")
        self.assertEqual(len(dev["relationship_maps"]), 1)
        rm = dev["relationship_maps"][0]
        self.assertIsInstance(rm["source_model"], dict)
        self.assertIn("app_label", rm["source_model"])
        for key in ("target_field", "coalesce", "template"):
            self.assertIn(key, rm)

    def test_json_serialisable(self):
        json.dumps(self._export())  # must not raise

    def test_default_queryset_covers_all(self):
        total = IPFabricTransformMap.objects.count()
        self.assertEqual(len(export_transform_maps()), total)


# ---------------------------------------------------------------------------
# Round-trip
# ---------------------------------------------------------------------------


class RoundTripTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.group = IPFabricTransformMapGroup.objects.create(name="RT Group")
        cls.site_ep = _site_ep()
        cls.device_ep = _device_ep()
        cls.site_ct = _ct("dcim", "site")
        cls.device_ct = _ct("dcim", "device")
        cls.mfr_ct = _ct("dcim", "manufacturer")

        cls.site_map = IPFabricTransformMap.objects.create(
            name="RT Site",
            source_endpoint=cls.site_ep,
            target_model=cls.site_ct,
            group=cls.group,
        )
        IPFabricTransformField.objects.create(
            transform_map=cls.site_map,
            source_field="siteName",
            target_field="name",
            coalesce=False,
            template="",
        )

        cls.mfr_map = IPFabricTransformMap.objects.create(
            name="RT Manufacturer",
            source_endpoint=cls.device_ep,
            target_model=cls.mfr_ct,
            group=cls.group,
        )
        cls.mfr_map.parents.add(cls.site_map)

        cls.device_map = IPFabricTransformMap.objects.create(
            name="RT Device",
            source_endpoint=cls.device_ep,
            target_model=cls.device_ct,
            group=cls.group,
        )
        cls.device_map.parents.add(cls.site_map, cls.mfr_map)
        IPFabricRelationshipField.objects.create(
            transform_map=cls.device_map,
            source_model=cls.site_ct,
            target_field="site",
            coalesce=True,
            template="{{ object.siteName }}",
        )

    def _do_round_trip(self):
        qs = IPFabricTransformMap.objects.filter(group=self.group)
        exported = export_transform_maps(queryset=qs)
        qs.delete()
        build_transform_maps(exported)
        return IPFabricTransformMap.objects.filter(group__name="RT Group")

    def test_map_count(self):
        qs = self._do_round_trip()
        self.assertEqual(qs.count(), 3)

    def test_field_maps_preserved(self):
        self._do_round_trip()
        site = IPFabricTransformMap.objects.get(name="RT Site", group__name="RT Group")
        self.assertEqual(site.field_maps.count(), 1)
        self.assertEqual(site.field_maps.first().source_field, "siteName")

    def test_relationship_maps_preserved(self):
        self._do_round_trip()
        device = IPFabricTransformMap.objects.get(
            name="RT Device", group__name="RT Group"
        )
        self.assertEqual(device.relationship_maps.count(), 1)
        self.assertEqual(device.relationship_maps.first().target_field, "site")

    def test_parents_rewired(self):
        self._do_round_trip()
        mfr = IPFabricTransformMap.objects.get(
            name="RT Manufacturer", group__name="RT Group"
        )
        device = IPFabricTransformMap.objects.get(
            name="RT Device", group__name="RT Group"
        )
        site = IPFabricTransformMap.objects.get(name="RT Site", group__name="RT Group")
        self.assertIn(site, mfr.parents.all())
        self.assertIn(site, device.parents.all())
        self.assertIn(mfr, device.parents.all())

    def test_cross_group_parent_preserved(self):
        other_grp = IPFabricTransformMapGroup.objects.create(name="RT Other")
        vrf_ep = IPFabricEndpoint.objects.get(endpoint="/technology/routing/vrf/detail")
        parent_other = IPFabricTransformMap.objects.create(
            name="RT VRF Parent",
            source_endpoint=vrf_ep,
            target_model=_ct("ipam", "vrf"),
            group=other_grp,
        )
        cross = IPFabricTransformMap.objects.create(
            name="RT Cross Child",
            source_endpoint=IPFabricEndpoint.objects.get(
                endpoint="/technology/addressing/managed-ip/ipv4"
            ),
            target_model=_ct("ipam", "ipaddress"),
            group=self.group,
        )
        cross.parents.add(parent_other)

        exported = export_transform_maps(
            queryset=IPFabricTransformMap.objects.filter(pk=cross.pk)
        )
        cross.delete()
        build_transform_maps(exported)

        reimported = IPFabricTransformMap.objects.get(
            name="RT Cross Child", group__name="RT Group"
        )
        self.assertIn(parent_other, reimported.parents.all())
