from unittest.mock import Mock
from unittest.mock import patch

from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from rest_framework import status
from utilities.testing import APIViewTestCases

from ipfabric_netbox.choices import IPFabricFilterTypeChoices
from ipfabric_netbox.choices import IPFabricSourceStatusChoices
from ipfabric_netbox.choices import IPFabricSyncStatusChoices
from ipfabric_netbox.models import IPFabricData
from ipfabric_netbox.models import IPFabricEndpoint
from ipfabric_netbox.models import IPFabricFilter
from ipfabric_netbox.models import IPFabricFilterExpression
from ipfabric_netbox.models import IPFabricIngestion
from ipfabric_netbox.models import IPFabricIngestionIssue
from ipfabric_netbox.models import IPFabricRelationshipField
from ipfabric_netbox.models import IPFabricSnapshot
from ipfabric_netbox.models import IPFabricSource
from ipfabric_netbox.models import IPFabricSync
from ipfabric_netbox.models import IPFabricTransformField
from ipfabric_netbox.models import IPFabricTransformMap
from ipfabric_netbox.models import IPFabricTransformMapGroup


BASE = "/api/plugins/ipfabric"


class IPFabricTransformMapGroupTest(APIViewTestCases.APIViewTestCase):
    model = IPFabricTransformMapGroup
    graphql_base_name = "ipfabric_transform_map_group"
    brief_fields = [
        "description",
        "display",
        "id",
        "name",
    ]
    create_data = [
        {"name": "Group A"},
        {"name": "Group B", "description": "Description of group B"},
        {"name": "Group C"},
    ]
    bulk_update_data = {
        "description": "Updated Group Description",
    }

    def _get_list_url(self):
        return f"{BASE}/transform-map-group/"

    def _get_detail_url(self, instance):
        return f"{BASE}/transform-map-group/{instance.pk}/"

    @classmethod
    def setUpTestData(cls):
        IPFabricTransformMapGroup.objects.create(name="Group D")
        IPFabricTransformMapGroup.objects.create(
            name="Group E", description="Description of group E"
        )
        IPFabricTransformMapGroup.objects.create(name="Group F")


class IPFabricTransformMapTest(APIViewTestCases.APIViewTestCase):
    model = IPFabricTransformMap
    graphql_base_name = "ipfabric_transform_map"
    brief_fields = [
        "display",
        "group",
        "id",
        "name",
        "source_endpoint",
        "target_model",
    ]

    def _get_list_url(self):
        return f"{BASE}/transform-map/"

    def _get_detail_url(self, instance):
        return f"{BASE}/transform-map/{instance.pk}/"

    @classmethod
    def setUpTestData(cls):
        groups = (
            IPFabricTransformMapGroup.objects.create(name="Group A"),
            IPFabricTransformMapGroup.objects.create(name="Group B"),
            IPFabricTransformMapGroup.objects.create(name="Group C"),
        )

        # Get existing endpoints created by migrations
        endpoints = {
            "site": IPFabricEndpoint.objects.get(endpoint="/inventory/sites/overview"),
            "device": IPFabricEndpoint.objects.get(endpoint="/inventory/devices"),
            "ipaddress": IPFabricEndpoint.objects.get(
                endpoint="/technology/addressing/managed-ip/ipv4"
            ),
            "vrf": IPFabricEndpoint.objects.get(
                endpoint="/technology/routing/vrf/detail"
            ),
        }

        IPFabricTransformMap.objects.create(
            name="TransformMap D",
            source_endpoint=endpoints["site"],
            target_model=ContentType.objects.get(app_label="dcim", model="site"),
        )
        IPFabricTransformMap.objects.create(
            name="TransformMap E",
            source_endpoint=endpoints["site"],
            target_model=ContentType.objects.get(app_label="dcim", model="site"),
            group=groups[0],
        )
        IPFabricTransformMap.objects.create(
            name="TransformMap F",
            source_endpoint=endpoints["ipaddress"],
            target_model=ContentType.objects.get(app_label="ipam", model="ipaddress"),
            group=groups[0],
        )

        cls.create_data = [
            {
                "name": "Transform Map A",
                "source_endpoint": endpoints["site"].pk,
                "target_model": "dcim.site",
                "group": groups[1].pk,
            },
            {
                "name": "Transform Map B",
                "source_endpoint": endpoints["device"].pk,
                "target_model": "dcim.device",
                "group": groups[1].pk,
            },
            {
                "name": "Transform Map C",
                "source_endpoint": endpoints["vrf"].pk,
                "target_model": "ipam.vrf",
                "group": groups[1].pk,
            },
        ]
        cls.bulk_update_data = {
            "group": groups[2].pk,
        }


class IPFabricTransformFieldTest(APIViewTestCases.APIViewTestCase):
    model = IPFabricTransformField
    graphql_base_name = "ipfabric_transform_field"
    # in this case brief fields are the same, but they are needed fot the test
    brief_fields = [
        "coalesce",
        "display",
        "id",
        "source_field",
        "target_field",
        "template",
        "transform_map",
    ]

    def _get_list_url(self):
        return f"{BASE}/transform-field/"

    def _get_detail_url(self, instance):
        return f"{BASE}/transform-field/{instance.pk}/"

    @classmethod
    def setUpTestData(cls):
        # Create groups for transform maps
        groups = (
            IPFabricTransformMapGroup.objects.create(name="Field Test Group A"),
            IPFabricTransformMapGroup.objects.create(name="Field Test Group B"),
            IPFabricTransformMapGroup.objects.create(name="Field Test Group C"),
        )

        # Get existing endpoints created by migrations
        endpoints = {
            "site": IPFabricEndpoint.objects.get(endpoint="/inventory/sites/overview"),
            "device": IPFabricEndpoint.objects.get(endpoint="/inventory/devices"),
            "ipaddress": IPFabricEndpoint.objects.get(
                endpoint="/technology/addressing/managed-ip/ipv4"
            ),
        }

        # Create transform maps for the fields to reference
        transform_maps = [
            IPFabricTransformMap.objects.create(
                name="Field Map A",
                source_endpoint=endpoints["site"],
                target_model=ContentType.objects.get(app_label="dcim", model="site"),
                group=groups[0],
            ),
            IPFabricTransformMap.objects.create(
                name="Field Map B",
                source_endpoint=endpoints["device"],
                target_model=ContentType.objects.get(app_label="dcim", model="device"),
                group=groups[0],
            ),
            IPFabricTransformMap.objects.create(
                name="Field Map C",
                source_endpoint=endpoints["ipaddress"],
                target_model=ContentType.objects.get(
                    app_label="ipam", model="ipaddress"
                ),
                group=groups[1],
            ),
        ]

        # Create existing transform fields for testing
        IPFabricTransformField.objects.create(
            transform_map=transform_maps[0],
            source_field="hostname",
            target_field="name",
            coalesce=False,
            template="{{ hostname }}",
        )
        IPFabricTransformField.objects.create(
            transform_map=transform_maps[0],
            source_field="site_name",
            target_field="description",
            coalesce=True,
            template="Site: {{ site_name }}",
        )
        IPFabricTransformField.objects.create(
            transform_map=transform_maps[1],
            source_field="device_type",
            target_field="platform",
            coalesce=False,
            template="",
        )

        cls.create_data = [
            {
                "transform_map": transform_maps[1].pk,
                "source_field": "ip_address",
                "target_field": "primary_ip4",
                "coalesce": False,
                "template": "{{ ip_address }}",
            },
            {
                "transform_map": transform_maps[1].pk,
                "source_field": "location",
                "target_field": "site",
                "coalesce": True,
                "template": "{{ location | default('Unknown') }}",
            },
            {
                "transform_map": transform_maps[2].pk,
                "source_field": "subnet",
                "target_field": "address",
                "coalesce": False,
                "template": "{{ subnet }}",
            },
        ]
        cls.bulk_update_data = {
            "coalesce": True,
        }


class IPFabricRelationshipFieldTest(APIViewTestCases.APIViewTestCase):
    model = IPFabricRelationshipField
    graphql_base_name = "ipfabric_relationship_field"
    # in this case brief fields are the same, but they are needed fot the test
    brief_fields = [
        "coalesce",
        "display",
        "id",
        "source_model",
        "target_field",
        "template",
        "transform_map",
    ]

    def _get_list_url(self):
        return f"{BASE}/relationship-field/"

    def _get_detail_url(self, instance):
        return f"{BASE}/relationship-field/{instance.pk}/"

    @classmethod
    def setUpTestData(cls):
        # Create groups for transform maps
        groups = (
            IPFabricTransformMapGroup.objects.create(name="Relationship Test Group A"),
            IPFabricTransformMapGroup.objects.create(name="Relationship Test Group B"),
            IPFabricTransformMapGroup.objects.create(name="Relationship Test Group C"),
        )

        # Get existing endpoints created by migrations
        endpoints = {
            "site": IPFabricEndpoint.objects.get(endpoint="/inventory/sites/overview"),
            "device": IPFabricEndpoint.objects.get(endpoint="/inventory/devices"),
            "ipaddress": IPFabricEndpoint.objects.get(
                endpoint="/technology/addressing/managed-ip/ipv4"
            ),
        }

        # Create transform maps for the relationship fields to reference
        transform_maps = [
            IPFabricTransformMap.objects.create(
                name="Relationship Map A",
                source_endpoint=endpoints["site"],
                target_model=ContentType.objects.get(app_label="dcim", model="site"),
                group=groups[0],
            ),
            IPFabricTransformMap.objects.create(
                name="Relationship Map B",
                source_endpoint=endpoints["device"],
                target_model=ContentType.objects.get(app_label="dcim", model="device"),
                group=groups[0],
            ),
            IPFabricTransformMap.objects.create(
                name="Relationship Map C",
                source_endpoint=endpoints["ipaddress"],
                target_model=ContentType.objects.get(
                    app_label="ipam", model="ipaddress"
                ),
                group=groups[1],
            ),
        ]

        # Create existing relationship fields for testing
        IPFabricRelationshipField.objects.create(
            transform_map=transform_maps[0],
            source_model=ContentType.objects.get(app_label="dcim", model="site"),
            target_field="location",
            coalesce=False,
            template="{{ site.location_id }}",
        )
        IPFabricRelationshipField.objects.create(
            transform_map=transform_maps[0],
            source_model=ContentType.objects.get(app_label="dcim", model="device"),
            target_field="site",
            coalesce=True,
            template="{{ device.site_id }}",
        )
        IPFabricRelationshipField.objects.create(
            transform_map=transform_maps[1],
            source_model=ContentType.objects.get(app_label="ipam", model="ipaddress"),
            target_field="interface",
            coalesce=False,
            template="{{ ipaddress.interface_id }}",
        )

        cls.create_data = [
            {
                "transform_map": transform_maps[1].pk,
                "source_model": "dcim.device",
                "target_field": "primary_ip4",
                "coalesce": False,
                "template": "{{ device.primary_ip4_id }}",
            },
            {
                "transform_map": transform_maps[1].pk,
                "source_model": "dcim.interface",
                "target_field": "device",
                "coalesce": True,
                "template": "{{ interface.device_id }}",
            },
            {
                "transform_map": transform_maps[2].pk,
                "source_model": "ipam.prefix",
                "target_field": "vrf",
                "coalesce": False,
                "template": "{{ prefix.vrf_id }}",
            },
        ]
        cls.bulk_update_data = {
            "coalesce": True,
        }


class IPFabricSourceTest(APIViewTestCases.APIViewTestCase):
    model = IPFabricSource
    brief_fields = [
        "display",
        "id",
        "name",
        "status",
        "type",
        "url",
    ]
    bulk_update_data = {
        "url": "https://updated.local",
    }
    graphql_base_name = "ipfabric_source"

    def _get_list_url(self):
        return f"{BASE}/source/"

    def _get_detail_url(self, instance):
        return f"{BASE}/source/{instance.pk}/"

    @classmethod
    def setUpTestData(cls):
        IPFabricSource.objects.create(
            name="Source A",
            url="https://a.local",
            parameters={"auth": "t", "verify": True},
            last_synced=timezone.now(),
        )
        IPFabricSource.objects.create(
            name="Source B",
            url="https://b.local",
            parameters={"auth": "t", "verify": False},
            last_synced=timezone.now(),
        )
        IPFabricSource.objects.create(
            name="Source C",
            url="https://c.local",
            parameters={"auth": "t", "verify": False},
            last_synced=timezone.now(),
        )

        cls.create_data = [
            {
                "name": "NewSrc 1",
                "url": "https://nb1.example",
                "parameters": {"auth": "t", "verify": False},
                "type": "local",
            },
            {
                "name": "NewSrc 2",
                "url": "https://nb2.example",
                "parameters": {"auth": "t", "verify": True},
                "type": "local",
            },
            {
                "name": "NewSrc 3",
                "url": "https://nb3.example",
                "parameters": {"auth": "t", "verify": True},
                "type": "local",
            },
        ]

    def test_sync_action_success(self):
        """Test successful sync action with proper permissions and ready source."""
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsource",
            "ipfabric_netbox.sync_ipfabricsource",
        )
        # Get the first source from setUpTestData
        source = IPFabricSource.objects.first()
        # Set status to make ready_for_sync return True
        source.status = IPFabricSourceStatusChoices.COMPLETED
        source.save()

        with self.settings(CELERY_TASK_ALWAYS_EAGER=True):
            # Create a mock job object to simulate enqueue_sync_job response
            mock_job = Mock()
            mock_job.id = "test-job-123"
            mock_job.status = "queued"

            with patch.object(source, "enqueue_sync_job", return_value=mock_job):
                url = f"{BASE}/source/{source.pk}/sync/"
                response = self.client.post(url, **self.header)

                self.assertHttpStatus(response, status.HTTP_201_CREATED)
                self.assertIn("id", response.data)

    def test_sync_action_permission_denied(self):
        """Test sync action without proper permissions."""
        # Note: Not adding sync_ipfabricsource permission
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsource",
        )

        source = IPFabricSource.objects.first()
        url = f"{BASE}/source/{source.pk}/sync/"
        response = self.client.post(url, **self.header)

        self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)

    def test_sync_action_not_ready(self):
        """Test sync action when source is not ready for sync."""
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsource",
            "ipfabric_netbox.sync_ipfabricsource",
        )

        source = IPFabricSource.objects.first()
        # Set status to make ready_for_sync return False
        source.status = IPFabricSourceStatusChoices.SYNCING
        source.save()

        url = f"{BASE}/source/{source.pk}/sync/"
        response = self.client.post(url, **self.header)

        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
        self.assertIn(
            f"Source '{source.name}' is not ready to be synced.", str(response.content)
        )


class IPFabricSnapshotTest(
    APIViewTestCases.GetObjectViewTestCase,
    APIViewTestCases.ListObjectsViewTestCase,
    APIViewTestCases.GraphQLTestCase,
):
    model = IPFabricSnapshot
    graphql_base_name = "ipfabric_snapshot"
    brief_fields = [
        "data",
        "date",
        "display",
        "id",
        "name",
        "snapshot_id",
        "source",
        "status",
    ]

    def _get_list_url(self):
        return f"{BASE}/snapshot/"

    def _get_detail_url(self, instance):
        return f"{BASE}/snapshot/{instance.pk}/"

    @classmethod
    def setUpTestData(cls):
        sources = (
            IPFabricSource.objects.create(
                name="Source A",
                url="https://src.local",
                parameters={"auth": "t", "verify": True},
                last_synced=timezone.now(),
            ),
            IPFabricSource.objects.create(
                name="Source B",
                url="https://srcb.local",
                parameters={"auth": "t", "verify": True},
                last_synced=timezone.now(),
            ),
            IPFabricSource.objects.create(
                name="Source C",
                url="https://srcc.local",
                parameters={"auth": "t", "verify": True},
                last_synced=timezone.now(),
            ),
        )

        cls.snapshots = (
            IPFabricSnapshot.objects.create(
                name="Snapshot One",
                source=sources[0],
                snapshot_id="snap-1",
                status="loaded",
                data={"sites": ["SiteA", "SiteB", "RemoteC"]},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
            IPFabricSnapshot.objects.create(
                name="Another Name",
                source=sources[0],
                snapshot_id="snap-2",
                status="loaded",
                data={"sites": []},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
            IPFabricSnapshot.objects.create(
                name="Third Snapshot",
                source=sources[0],
                snapshot_id="snap-3",
                status="unloaded",
                data={"sites": ["SiteD"]},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
        )

    def test_sites_action_lists_all_and_filters(self):
        self.add_permissions("ipfabric_netbox.view_ipfabricsnapshot")
        # list all
        url = f"{BASE}/snapshot/{self.snapshots[0].pk}/sites/"
        resp = self.client.get(url, **self.header)
        self.assertHttpStatus(resp, status.HTTP_200_OK)
        body = resp.json()
        self.assertIn(body.__class__, (list, dict))
        if isinstance(body, dict):
            labels = [i["name"] for i in body["results"]]
            self.assertEqual(labels, self.snapshots[0].data["sites"])
        # filter
        url = f"{BASE}/snapshot/{self.snapshots[0].pk}/sites/?q=site"
        resp = self.client.get(url, **self.header)
        self.assertHttpStatus(resp, status.HTTP_200_OK)
        body = resp.json()
        if isinstance(body, dict) and body.get("results"):
            labels = [i["name"].lower() for i in body["results"]]
            self.assertTrue(all("site" in name for name in labels))

    def test_sites_action_with_no_data(self):
        """Test sites endpoint when snapshot.data is None."""
        self.add_permissions("ipfabric_netbox.view_ipfabricsnapshot")

        # Create a snapshot with data=None
        source = IPFabricSource.objects.first()
        snapshot_no_data = IPFabricSnapshot.objects.create(
            name="Snapshot No Data",
            source=source,
            snapshot_id="snap-no-data",
            status="unloaded",
            data=None,
            date=timezone.now(),
            last_updated=timezone.now(),
        )

        # Call sites endpoint on snapshot with no data
        url = f"{BASE}/snapshot/{snapshot_no_data.pk}/sites/"
        resp = self.client.get(url, **self.header)
        self.assertHttpStatus(resp, status.HTTP_200_OK)
        body = resp.json()
        # Should return empty list when data is None
        self.assertEqual(body, [])

    def test_raw_patch_and_delete(self):
        self.add_permissions(
            "ipfabric_netbox.view_ipfabricsnapshot",
            "ipfabric_netbox.change_ipfabricsnapshot",
            "ipfabric_netbox.delete_ipfabricsnapshot",
        )
        # initial count
        self.assertEqual(
            IPFabricData.objects.filter(snapshot_data=self.snapshots[0]).count(), 0
        )
        # PATCH raw
        url = f"{BASE}/snapshot/{self.snapshots[0].pk}/raw/"
        payload = {
            "data": [
                {"data": {"example": 1}, "type": "device"},
                {"data": {"foo": "bar"}, "type": "interface"},
            ]
        }
        resp = self.client.patch(url, data=payload, format="json", **self.header)
        self.assertHttpStatus(resp, status.HTTP_200_OK)
        self.assertEqual(resp.data, {"status": "success"})
        self.assertEqual(
            IPFabricData.objects.filter(snapshot_data=self.snapshots[0]).count(), 2
        )
        # DELETE raw
        resp = self.client.delete(url, **self.header)
        self.assertHttpStatus(resp, status.HTTP_200_OK)
        self.assertEqual(resp.data, {"status": "success"})
        self.assertEqual(
            IPFabricData.objects.filter(snapshot_data=self.snapshots[0]).count(), 0
        )


class IPFabricSyncTest(APIViewTestCases.APIViewTestCase):
    model = IPFabricSync
    graphql_base_name = "ipfabric_sync"
    brief_fields = [
        "auto_merge",
        "display",
        "id",
        "last_synced",
        "name",
        "parameters",
        "status",
        "update_custom_fields",
    ]
    create_data = [
        {
            "name": "Test Sync A",
            "parameters": {"dcim.site": True, "dcim.device": False},
            "filters": [],
        },
        {
            "name": "Test Sync B",
            "parameters": {"ipam.ipaddress": True, "ipam.prefix": True},
            "auto_merge": True,
            "update_custom_fields": True,
            "filters": [],
        },
        {
            "name": "Test Sync C",
            "parameters": {"dcim.device": True, "dcim.interface": True},
            "interval": 60,
            "filters": [],
        },
    ]
    bulk_update_data = {
        "auto_merge": True,
        "update_custom_fields": False,
    }

    def _get_list_url(self):
        return f"{BASE}/sync/"

    def _get_detail_url(self, instance):
        return f"{BASE}/sync/{instance.pk}/"

    @classmethod
    def setUpTestData(cls):
        # Create sources for the snapshots
        sources = (
            IPFabricSource.objects.create(
                name="Sync Test Source A",
                url="https://sync-a.local",
                parameters={"auth": "token", "verify": True},
                last_synced=timezone.now(),
            ),
            IPFabricSource.objects.create(
                name="Sync Test Source B",
                url="https://sync-b.local",
                parameters={"auth": "token", "verify": False},
                last_synced=timezone.now(),
            ),
            IPFabricSource.objects.create(
                name="Sync Test Source C",
                url="https://sync-c.local",
                parameters={"auth": "token", "verify": True},
                last_synced=timezone.now(),
            ),
        )

        # Create snapshots for the syncs
        snapshots = (
            IPFabricSnapshot.objects.create(
                name="Sync Test Snapshot A",
                source=sources[0],
                snapshot_id="sync-snap-a",
                status="loaded",
                data={"sites": ["SyncSiteA", "SyncSiteB"]},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
            IPFabricSnapshot.objects.create(
                name="Sync Test Snapshot B",
                source=sources[1],
                snapshot_id="sync-snap-b",
                status="loaded",
                data={"devices": ["SyncDevice1", "SyncDevice2"]},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
            IPFabricSnapshot.objects.create(
                name="Sync Test Snapshot C",
                source=sources[2],
                snapshot_id="sync-snap-c",
                status="unloaded",
                data={"interfaces": ["SyncInterface1"]},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
        )

        # Create syncs for testing
        IPFabricSync.objects.create(
            name="Sync Test D",
            snapshot_data=snapshots[0],
            parameters={"dcim.site": True, "dcim.device": False},
        )
        IPFabricSync.objects.create(
            name="Sync Test E",
            snapshot_data=snapshots[1],
            parameters={"dcim.device": True, "dcim.interface": True},
            auto_merge=False,
            update_custom_fields=True,
        )
        IPFabricSync.objects.create(
            name="Sync Test F",
            snapshot_data=snapshots[2],
            parameters={"ipam.ipaddress": True, "ipam.prefix": False},
            interval=30,
        )

        # Update create_data to reference the snapshots
        cls.create_data[0]["snapshot_data"] = snapshots[0].pk
        cls.create_data[1]["snapshot_data"] = snapshots[1].pk
        cls.create_data[2]["snapshot_data"] = snapshots[2].pk
        cls.create_data[0]["parameters"] = {"dcim.site": True, "dcim.device": False}
        cls.create_data[1]["parameters"] = {"ipam.ipaddress": True, "ipam.prefix": True}
        cls.create_data[2]["parameters"] = {"dcim.device": True, "dcim.interface": True}

    def test_sync_action_success(self):
        """Test successful sync action with proper permissions and ready sync."""
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsync",
            "ipfabric_netbox.sync_ipfabricsync",
        )
        # Get the first sync from setUpTestData
        sync = IPFabricSync.objects.first()
        # Set status and ensure snapshot has data to make ready_for_sync return True
        sync.status = IPFabricSyncStatusChoices.COMPLETED
        sync.save()

        # Ensure the snapshot has data
        sync.snapshot_data.source.type = (
            "local"  # For local type, ready_for_sync checks are simpler
        )
        sync.snapshot_data.source.save()

        with self.settings(CELERY_TASK_ALWAYS_EAGER=True):
            # Create a mock job object to simulate enqueue_sync_job response
            mock_job = Mock()
            mock_job.id = "test-sync-job-456"
            mock_job.status = "queued"

            with patch.object(sync, "enqueue_sync_job", return_value=mock_job):
                url = f"{BASE}/sync/{sync.pk}/sync/"
                response = self.client.post(url, **self.header)

                self.assertHttpStatus(response, status.HTTP_201_CREATED)
                self.assertIn("id", response.data)

    def test_sync_action_permission_denied(self):
        """Test sync action without proper permissions."""
        # Note: Not adding sync_ipfabricsource permission
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsync",
        )

        sync = IPFabricSync.objects.first()
        url = f"{BASE}/sync/{sync.pk}/sync/"
        response = self.client.post(url, **self.header)

        self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)

    def test_sync_action_not_ready(self):
        """Test sync action when sync is not ready for sync."""
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsync",
            "ipfabric_netbox.sync_ipfabricsync",
        )

        sync = IPFabricSync.objects.first()
        # Set status to make ready_for_sync return False
        sync.status = IPFabricSyncStatusChoices.SYNCING
        sync.save()

        url = f"{BASE}/sync/{sync.pk}/sync/"
        response = self.client.post(url, **self.header)

        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
        self.assertIn(
            f"Sync '{sync.name}' is not ready to be synced.", str(response.content)
        )

    def test_parameters_validation_invalid_key_typo(self):
        """Test that typos in parameter keys are rejected."""
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsync",
            "ipfabric_netbox.change_ipfabricsync",
        )

        # Test invalid key "site" instead of "sites"
        invalid_data = self.create_data[0].copy()
        invalid_data["parameters"] = {"site": ["Site1"]}  # Should be "sites"

        response = self.client.post(
            self._get_list_url(), invalid_data, format="json", **self.header
        )
        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
        self.assertIn("site", str(response.data))
        self.assertIn("Invalid parameter key", str(response.data))

    def test_parameters_validation_invalid_contenttype_format(self):
        """Test that invalid ContentType format is rejected."""
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsync",
            "ipfabric_netbox.change_ipfabricsync",
        )

        # Test invalid format "device" instead of "dcim.device"
        invalid_data = self.create_data[0].copy()
        invalid_data["parameters"] = {"device": True}  # Should be "dcim.device"

        response = self.client.post(
            self._get_list_url(), invalid_data, format="json", **self.header
        )
        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
        self.assertIn("device", str(response.data))
        self.assertIn("Invalid parameter key", str(response.data))

    def test_parameters_validation_nonexistent_contenttype(self):
        """Test that non-existent ContentType is rejected."""
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsync",
            "ipfabric_netbox.change_ipfabricsync",
        )

        # Test non-existent ContentType
        invalid_data = self.create_data[0].copy()
        invalid_data["parameters"] = {"invalid.model": True}

        response = self.client.post(
            self._get_list_url(), invalid_data, format="json", **self.header
        )
        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
        self.assertIn("invalid.model", str(response.data))
        self.assertIn("does not exist", str(response.data))

    def test_parameters_validation_wrong_value_type_sites(self):
        """Test that wrong value type for 'sites' is rejected."""
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsync",
            "ipfabric_netbox.change_ipfabricsync",
        )

        # Test wrong type for sites (string instead of list)
        invalid_data = self.create_data[0].copy()
        invalid_data["parameters"] = {"sites": "Site1"}  # Should be a list

        response = self.client.post(
            self._get_list_url(), invalid_data, format="json", **self.header
        )
        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
        self.assertIn("sites", str(response.data))
        self.assertIn("must be a list", str(response.data))

    def test_parameters_validation_wrong_value_type_groups(self):
        """Test that wrong value type for 'groups' is rejected."""
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsync",
            "ipfabric_netbox.change_ipfabricsync",
        )

        # Test wrong type for groups (string instead of list)
        invalid_data = self.create_data[0].copy()
        invalid_data["parameters"] = {"groups": "1"}  # Should be a list of integers

        response = self.client.post(
            self._get_list_url(), invalid_data, format="json", **self.header
        )
        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
        self.assertIn("groups", str(response.data))
        self.assertIn("must be a list", str(response.data))

    def test_parameters_validation_groups_non_integer_values(self):
        """Test that non-integer values in groups list are rejected."""
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsync",
            "ipfabric_netbox.change_ipfabricsync",
        )

        # Test non-integer values in groups list
        invalid_data = self.create_data[0].copy()
        invalid_data["parameters"] = {
            "groups": ["1", "2"]
        }  # Should be integers, not strings

        response = self.client.post(
            self._get_list_url(), invalid_data, format="json", **self.header
        )
        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
        self.assertIn("groups", str(response.data))
        self.assertIn("list of integers", str(response.data))

    def test_parameters_validation_wrong_value_type_contenttype(self):
        """Test that wrong value type for ContentType keys is rejected."""
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsync",
            "ipfabric_netbox.change_ipfabricsync",
        )

        # Test wrong type for ContentType (string instead of boolean)
        invalid_data = self.create_data[0].copy()
        invalid_data["parameters"] = {"dcim.device": "true"}  # Should be a boolean

        response = self.client.post(
            self._get_list_url(), invalid_data, format="json", **self.header
        )
        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
        self.assertIn("dcim.device", str(response.data))
        self.assertIn("must be a boolean", str(response.data))

    def test_parameters_validation_valid_parameters(self):
        """Test that valid parameters pass validation."""
        self.add_permissions(
            "ipfabric_netbox.add_ipfabricsync",
            "ipfabric_netbox.change_ipfabricsync",
        )

        # Test valid parameters
        valid_data = self.create_data[0].copy()
        valid_data["parameters"] = {
            "sites": ["Site1", "Site2"],
            "groups": [1, 2],
            "dcim.device": True,
            "dcim.interface": False,
            "ipam.vlan": True,
        }

        response = self.client.post(
            self._get_list_url(), valid_data, format="json", **self.header
        )
        self.assertHttpStatus(response, status.HTTP_201_CREATED)
        self.assertEqual(response.data["parameters"]["sites"], ["Site1", "Site2"])
        self.assertEqual(response.data["parameters"]["groups"], [1, 2])
        self.assertEqual(response.data["parameters"]["dcim.device"], True)


class IPFabricIngestionTest(
    APIViewTestCases.GetObjectViewTestCase,
    APIViewTestCases.ListObjectsViewTestCase,
    APIViewTestCases.GraphQLTestCase,
):
    model = IPFabricIngestion
    graphql_base_name = "ipfabric_ingestion"
    brief_fields = [
        "branch",
        "display",
        "id",
        "name",
        "sync",
    ]

    def _get_list_url(self):
        return f"{BASE}/ingestion/"

    def _get_detail_url(self, instance):
        return f"{BASE}/ingestion/{instance.pk}/"

    @classmethod
    def setUpTestData(cls):
        # Create sources for the snapshots
        sources = (
            IPFabricSource.objects.create(
                name="Ingestion Test Source A",
                url="https://ingestion-a.local",
                parameters={"auth": "token", "verify": True},
                last_synced=timezone.now(),
            ),
            IPFabricSource.objects.create(
                name="Ingestion Test Source B",
                url="https://ingestion-b.local",
                parameters={"auth": "token", "verify": False},
                last_synced=timezone.now(),
            ),
            IPFabricSource.objects.create(
                name="Ingestion Test Source C",
                url="https://ingestion-c.local",
                parameters={"auth": "token", "verify": True},
                last_synced=timezone.now(),
            ),
        )

        # Create snapshots for the syncs
        snapshots = (
            IPFabricSnapshot.objects.create(
                name="Ingestion Test Snapshot A",
                source=sources[0],
                snapshot_id="ing-snap-a",
                status="loaded",
                data={"sites": ["SiteA", "SiteB"]},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
            IPFabricSnapshot.objects.create(
                name="Ingestion Test Snapshot B",
                source=sources[1],
                snapshot_id="ing-snap-b",
                status="loaded",
                data={"devices": ["Device1", "Device2"]},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
            IPFabricSnapshot.objects.create(
                name="Ingestion Test Snapshot C",
                source=sources[2],
                snapshot_id="ing-snap-c",
                status="unloaded",
                data={"interfaces": ["Interface1"]},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
        )

        # Create syncs for the ingestions
        syncs = (
            IPFabricSync.objects.create(
                name="Ingestion Test Sync A",
                snapshot_data=snapshots[0],
                parameters={"dcim.site": True, "dcim.device": False},
            ),
            IPFabricSync.objects.create(
                name="Ingestion Test Sync B",
                snapshot_data=snapshots[1],
                parameters={"dcim.device": True, "dcim.interface": True},
            ),
            IPFabricSync.objects.create(
                name="Ingestion Test Sync C",
                snapshot_data=snapshots[2],
                parameters={"ipam.ipaddress": True, "ipam.prefix": False},
            ),
        )

        # Create ingestions for testing
        IPFabricIngestion.objects.create(sync=syncs[0])
        IPFabricIngestion.objects.create(sync=syncs[1])
        IPFabricIngestion.objects.create(sync=syncs[2])


class IPFabricIngestionIssueTest(
    APIViewTestCases.GetObjectViewTestCase,
    APIViewTestCases.ListObjectsViewTestCase,
    APIViewTestCases.GraphQLTestCase,
):
    model = IPFabricIngestionIssue
    graphql_base_name = "ipfabric_ingestion_issue"
    brief_fields = [
        "display",
        "exception",
        "id",
        "ingestion",
        "message",
        "model",
    ]

    def _get_list_url(self):
        return f"{BASE}/ingestion-issues/"

    def _get_detail_url(self, instance):
        return f"{BASE}/ingestion-issues/{instance.pk}/"

    @classmethod
    def setUpTestData(cls):
        # Create sources for the snapshots
        sources = (
            IPFabricSource.objects.create(
                name="Issue Test Source A",
                url="https://issue-a.local",
                parameters={"auth": "token", "verify": True},
                last_synced=timezone.now(),
            ),
            IPFabricSource.objects.create(
                name="Issue Test Source B",
                url="https://issue-b.local",
                parameters={"auth": "token", "verify": False},
                last_synced=timezone.now(),
            ),
            IPFabricSource.objects.create(
                name="Issue Test Source C",
                url="https://issue-c.local",
                parameters={"auth": "token", "verify": True},
                last_synced=timezone.now(),
            ),
        )

        # Create snapshots for the syncs
        snapshots = (
            IPFabricSnapshot.objects.create(
                name="Issue Test Snapshot A",
                source=sources[0],
                snapshot_id="issue-snap-a",
                status="loaded",
                data={"sites": ["IssueTestSiteA", "IssueTestSiteB"]},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
            IPFabricSnapshot.objects.create(
                name="Issue Test Snapshot B",
                source=sources[1],
                snapshot_id="issue-snap-b",
                status="loaded",
                data={"devices": ["IssueTestDevice1", "IssueTestDevice2"]},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
            IPFabricSnapshot.objects.create(
                name="Issue Test Snapshot C",
                source=sources[2],
                snapshot_id="issue-snap-c",
                status="unloaded",
                data={"interfaces": ["IssueTestInterface1"]},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
        )

        # Create syncs for the ingestions
        syncs = (
            IPFabricSync.objects.create(
                name="Issue Test Sync A",
                snapshot_data=snapshots[0],
                parameters={"dcim.site": True, "dcim.device": False},
            ),
            IPFabricSync.objects.create(
                name="Issue Test Sync B",
                snapshot_data=snapshots[1],
                parameters={"dcim.device": True, "dcim.interface": True},
            ),
            IPFabricSync.objects.create(
                name="Issue Test Sync C",
                snapshot_data=snapshots[2],
                parameters={"ipam.ipaddress": True, "ipam.prefix": False},
            ),
        )

        # Create ingestions for the issues
        ingestions = (
            IPFabricIngestion.objects.create(sync=syncs[0]),
            IPFabricIngestion.objects.create(sync=syncs[1]),
            IPFabricIngestion.objects.create(sync=syncs[2]),
        )

        # Create ingestion issues for testing
        IPFabricIngestionIssue.objects.create(
            ingestion=ingestions[0],
            model="dcim.site",
            message="Failed to create site due to validation error",
            raw_data='{"name": "Invalid Site", "slug": ""}',
            coalesce_fields="name,slug",
            defaults="{}",
            exception="ValidationError: Slug field cannot be empty",
        )
        IPFabricIngestionIssue.objects.create(
            ingestion=ingestions[0],
            model="dcim.device",
            message="Device type not found",
            raw_data='{"hostname": "test-device", "device_type": "NonExistentType"}',
            coalesce_fields="hostname",
            defaults='{"status": "active"}',
            exception="DoesNotExist: DeviceType matching query does not exist",
        )
        IPFabricIngestionIssue.objects.create(
            ingestion=ingestions[1],
            model="dcim.interface",
            message="Interface creation failed - invalid MAC address",
            raw_data='{"name": "eth0", "mac_address": "invalid-mac", "device": 1}',
            coalesce_fields="name,device",
            defaults='{"type": "1000base-t"}',
            exception="ValidationError: Enter a valid MAC address",
        )
        IPFabricIngestionIssue.objects.create(
            ingestion=ingestions[2],
            model="ipam.ipaddress",
            message="IP address already exists",
            raw_data='{"address": "192.168.1.1/24", "status": "active"}',
            coalesce_fields="address",
            defaults='{"dns_name": ""}',
            exception="IntegrityError: IP address 192.168.1.1/24 already exists",
        )


class IPFabricEndpointTest(
    APIViewTestCases.GetObjectViewTestCase,
    APIViewTestCases.ListObjectsViewTestCase,
    APIViewTestCases.GraphQLTestCase,
):
    model = IPFabricEndpoint
    brief_fields = [
        "display",
        "endpoint",
        "id",
        "name",
    ]
    graphql_base_name = "ipfabric_endpoint"

    def _get_list_url(self):
        return f"{BASE}/endpoint/"

    def _get_detail_url(self, instance):
        return f"{BASE}/endpoint/{instance.pk}/"

    @classmethod
    def setUpTestData(cls):
        # Note: IPFabricEndpoint is read-only in the API (NetBoxReadOnlyModelViewSet)
        # The endpoints are created by migrations and should not be modified via API
        # We rely on the existing endpoints created by migrations for these tests
        pass


class IPFabricFilterTest(
    APIViewTestCases.GetObjectViewTestCase,
    APIViewTestCases.ListObjectsViewTestCase,
    APIViewTestCases.GraphQLTestCase,
):
    model = IPFabricFilter
    brief_fields = [
        "display",
        "endpoints",
        "expressions",
        "filter_type",
        "id",
        "name",
        "syncs",
    ]
    graphql_base_name = "ip_fabric_filter"

    def _get_list_url(self):
        return f"{BASE}/filter/"

    def _get_detail_url(self, instance):
        return f"{BASE}/filter/{instance.pk}/"

    @classmethod
    def setUpTestData(cls):
        # Get existing endpoints created by migrations
        endpoints = {
            "site": IPFabricEndpoint.objects.get(endpoint="/inventory/sites/overview"),
            "device": IPFabricEndpoint.objects.get(endpoint="/inventory/devices"),
            "ipaddress": IPFabricEndpoint.objects.get(
                endpoint="/technology/addressing/managed-ip/ipv4"
            ),
            "vrf": IPFabricEndpoint.objects.get(
                endpoint="/technology/routing/vrf/detail"
            ),
        }

        # Create sources for snapshots
        sources = (
            IPFabricSource.objects.create(
                name="Filter Test Source A",
                url="https://filter-a.local",
                parameters={"auth": "token", "verify": True},
                last_synced=timezone.now(),
            ),
            IPFabricSource.objects.create(
                name="Filter Test Source B",
                url="https://filter-b.local",
                parameters={"auth": "token", "verify": False},
                last_synced=timezone.now(),
            ),
        )

        # Create snapshots for syncs
        snapshots = (
            IPFabricSnapshot.objects.create(
                name="Filter Test Snapshot A",
                source=sources[0],
                snapshot_id="filter-snap-a",
                status="loaded",
                data={"sites": ["FilterSiteA"]},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
            IPFabricSnapshot.objects.create(
                name="Filter Test Snapshot B",
                source=sources[1],
                snapshot_id="filter-snap-b",
                status="loaded",
                data={"devices": ["FilterDevice1"]},
                date=timezone.now(),
                last_updated=timezone.now(),
            ),
        )

        # Create syncs to associate with filters
        syncs = (
            IPFabricSync.objects.create(
                name="Filter Test Sync A",
                snapshot_data=snapshots[0],
                parameters={"dcim.site": True, "dcim.device": False},
            ),
            IPFabricSync.objects.create(
                name="Filter Test Sync B",
                snapshot_data=snapshots[1],
                parameters={"dcim.device": True, "dcim.interface": True},
            ),
        )

        # Create existing filters for testing
        IPFabricFilter.objects.create(
            name="Filter D",
            description="Sites and devices filter",
            filter_type=IPFabricFilterTypeChoices.AND,
        )
        IPFabricFilter.objects.create(
            name="Filter E",
            description="Device and VRF filter",
            filter_type=IPFabricFilterTypeChoices.OR,
        )
        IPFabricFilter.objects.create(
            name="Filter F",
            description="IP address filter",
            filter_type=IPFabricFilterTypeChoices.AND,
        )

        cls.create_data = [
            {
                "name": "Filter A",
                "description": "Test filter A",
                "filter_type": IPFabricFilterTypeChoices.AND,
                "endpoints": [endpoints["site"].pk, endpoints["device"].pk],
                "syncs": [syncs[0].pk],
            },
            {
                "name": "Filter B",
                "description": "Test filter B",
                "filter_type": IPFabricFilterTypeChoices.OR,
                "endpoints": [endpoints["device"].pk],
                "syncs": [syncs[1].pk],
            },
            {
                "name": "Filter C",
                "description": "Test filter C",
                "filter_type": IPFabricFilterTypeChoices.AND,
                "endpoints": [endpoints["ipaddress"].pk, endpoints["vrf"].pk],
                "syncs": [],
            },
        ]
        cls.bulk_update_data = {
            "description": "Updated filter description",
        }


class IPFabricFilterExpressionTest(
    APIViewTestCases.GetObjectViewTestCase,
    APIViewTestCases.ListObjectsViewTestCase,
    APIViewTestCases.GraphQLTestCase,
):
    model = IPFabricFilterExpression
    brief_fields = [
        "display",
        "expression",
        "filters",
        "id",
        "name",
    ]
    graphql_base_name = "ip_fabric_filter_expression"

    def _get_list_url(self):
        return f"{BASE}/filter-expression/"

    def _get_detail_url(self, instance):
        return f"{BASE}/filter-expression/{instance.pk}/"

    @classmethod
    def setUpTestData(cls):
        # Create filters to associate with expressions
        filters = (
            IPFabricFilter.objects.create(
                name="Expression Test Filter A",
                description="First expression test filter",
                filter_type=IPFabricFilterTypeChoices.AND,
            ),
            IPFabricFilter.objects.create(
                name="Expression Test Filter B",
                description="Second expression test filter",
                filter_type=IPFabricFilterTypeChoices.OR,
            ),
            IPFabricFilter.objects.create(
                name="Expression Test Filter C",
                description="Third expression test filter",
                filter_type=IPFabricFilterTypeChoices.AND,
            ),
        )

        # Create existing filter expressions for testing
        IPFabricFilterExpression.objects.create(
            name="Expression D",
            description="Sites expression",
            expression=[{"siteName": ["eq", "Site1"]}],
        )
        IPFabricFilterExpression.objects.create(
            name="Expression E",
            description="Devices expression",
            expression=[{"hostname": ["like", "router%"]}],
        )
        IPFabricFilterExpression.objects.create(
            name="Expression F",
            description="Complex expression",
            expression=[
                {"siteName": ["eq", "Site1"]},
                {"hostname": ["like", "switch%"]},
            ],
        )

        cls.create_data = [
            {
                "name": "Expression A",
                "description": "Test expression A",
                "expression": [{"siteName": ["eq", "TestSite"]}],
                "filters": [filters[0].pk],
            },
            {
                "name": "Expression B",
                "description": "Test expression B",
                "expression": [{"hostname": ["like", "test-router%"]}],
                "filters": [filters[1].pk, filters[2].pk],
            },
            {
                "name": "Expression C",
                "description": "Test expression C",
                "expression": [
                    {"siteName": ["eq", "TestSite2"]},
                    {"vendor": ["eq", "Cisco"]},
                ],
                "filters": [filters[0].pk],
            },
        ]
        cls.bulk_update_data = {
            "description": "Updated expression description",
        }
