import csv
import json
from decimal import Decimal
from io import StringIO
from zoneinfo import ZoneInfo

import yaml
from django.contrib.contenttypes.models import ContentType
from django.http import StreamingHttpResponse
from django.test import override_settings, tag
from django.urls import reverse
from netaddr import EUI

from core.choices import ObjectChangeActionChoices
from core.models import ObjectChange, ObjectType
from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from extras.models import ConfigTemplate
from ipam.models import ASN, RIR, VLAN, VRF
from netbox.choices import CSVDelimiterChoices, ImportFormatChoices, WeightUnitChoices
from tenancy.models import Tenant
from users.models import ObjectPermission, User
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
from wireless.models import WirelessLAN


class RegionTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
    model = Region

    @classmethod
    def setUpTestData(cls):

        # Create three Regions
        regions = (
            Region(name='Region 1', slug='region-1', comments=''),
            Region(
                name='Region 2', slug='region-2', comments="It's going to take a lot to drag me away from you"
            ),
            Region(name='Region 3', slug='region-3'),
        )
        for region in regions:
            region.save()

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Region X',
            'slug': 'region-x',
            'parent': regions[2].pk,
            'description': 'A new region',
            'tags': [t.pk for t in tags],
            'comments': 'This comment is really exciting!',
        }

        cls.csv_data = (
            "name,slug,description,comments",
            "Region 4,region-4,Fourth region,",
            "Region 5,region-5,Fifth region,hi guys",
            "Region 6,region-6,Sixth region,bye guys",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{regions[0].pk},Region 7,Fourth region7",
            f"{regions[1].pk},Region 8,Fifth region8",
            f"{regions[2].pk},Region 0,Sixth region9",
        )

        cls.bulk_edit_data = {
            'description': 'New description',
            'comments': 'This comment is super exciting!!!',
        }


class SiteGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
    model = SiteGroup

    @classmethod
    def setUpTestData(cls):

        # Create three SiteGroups
        sitegroups = (
            SiteGroup(name='Site Group 1', slug='site-group-1', comments='Still here'),
            SiteGroup(name='Site Group 2', slug='site-group-2'),
            SiteGroup(name='Site Group 3', slug='site-group-3'),
        )
        for sitegroup in sitegroups:
            sitegroup.save()

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Site Group X',
            'slug': 'site-group-x',
            'parent': sitegroups[2].pk,
            'description': 'A new site group',
            'tags': [t.pk for t in tags],
            'comments': 'still here',
        }

        cls.csv_data = (
            "name,slug,description,comments",
            "Site Group 4,site-group-4,Fourth site group,",
            "Site Group 5,site-group-5,Fifth site group,still hear",
            "Site Group 6,site-group-6,Sixth site group,"
        )

        cls.csv_update_data = (
            "id,name,description,comments",
            f"{sitegroups[0].pk},Site Group 7,Fourth site group7,",
            f"{sitegroups[1].pk},Site Group 8,Fifth site group8,when will it end",
            f"{sitegroups[2].pk},Site Group 0,Sixth site group9,",
        )

        cls.bulk_edit_data = {
            'description': 'New description',
            'comments': 'the end',
        }


class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
    model = Site

    @classmethod
    def setUpTestData(cls):

        regions = (
            Region(name='Region 1', slug='region-1'),
            Region(name='Region 2', slug='region-2'),
        )
        for region in regions:
            region.save()

        groups = (
            SiteGroup(name='Site Group 1', slug='site-group-1'),
            SiteGroup(name='Site Group 2', slug='site-group-2'),
        )
        for group in groups:
            group.save()

        rir = RIR.objects.create(name='RFC 6996', is_private=True)

        asns = [
            ASN(asn=65000 + i, rir=rir) for i in range(8)
        ]
        ASN.objects.bulk_create(asns)

        sites = Site.objects.bulk_create([
            Site(name='Site 1', slug='site-1', region=regions[0], group=groups[1]),
            Site(name='Site 2', slug='site-2', region=regions[0], group=groups[1]),
            Site(name='Site 3', slug='site-3', region=regions[0], group=groups[1]),
        ])
        sites[0].asns.set([asns[0], asns[1]])
        sites[1].asns.set([asns[2], asns[3]])
        sites[2].asns.set([asns[4], asns[5]])

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Site X',
            'slug': 'site-x',
            'status': SiteStatusChoices.STATUS_PLANNED,
            'region': regions[1].pk,
            'group': groups[1].pk,
            'tenant': None,
            'facility': 'Facility X',
            'asns': [asns[6].pk, asns[7].pk],
            'time_zone': ZoneInfo('UTC'),
            'description': 'Site description',
            'physical_address': '742 Evergreen Terrace, Springfield, USA',
            'shipping_address': '742 Evergreen Terrace, Springfield, USA',
            'latitude': Decimal('35.780000'),
            'longitude': Decimal('-78.642000'),
            'comments': 'Test site',
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "name,slug,status",
            "Site 4,site-4,planned",
            "Site 5,site-5,active",
            "Site 6,site-6,staging",
        )

        cls.csv_update_data = (
            "id,name,status",
            f"{sites[0].pk},Site 7,staging",
            f"{sites[1].pk},Site 8,planned",
            f"{sites[2].pk},Site 9,active",
        )

        cls.bulk_edit_data = {
            'status': SiteStatusChoices.STATUS_PLANNED,
            'region': regions[1].pk,
            'group': groups[1].pk,
            'tenant': None,
            'time_zone': ZoneInfo('US/Eastern'),
            'description': 'New description',
        }

    def test_get_object_with_only_site_view_permission_hides_unauthorized_embedded_panels(self):
        site = self._get_queryset().first()

        obj_perm = ObjectPermission(
            name='Test permission',
            actions=['view'],
        )
        obj_perm.save()
        obj_perm.users.add(self.user)
        obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))

        response = self.client.get(site.get_absolute_url())
        self.assertHttpStatus(response, 200)

        for panel, url in (
            ('locations', reverse('dcim:location_list')),
            ('devices', reverse('dcim:device_list')),
            ('image attachments', reverse('extras:imageattachment_list')),
        ):
            with self.subTest(panel=panel):
                self.assertNotContains(response, url)


class LocationTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
    model = Location

    @classmethod
    def setUpTestData(cls):

        site = Site.objects.create(name='Site 1', slug='site-1')
        tenant = Tenant.objects.create(name='Tenant 1', slug='tenant-1')

        locations = (
            Location(
                name='Location 1',
                slug='location-1',
                site=site,
                status=LocationStatusChoices.STATUS_ACTIVE,
                tenant=tenant,
                comments='',
            ),
            Location(
                name='Location 2',
                slug='location-2',
                site=site,
                status=LocationStatusChoices.STATUS_ACTIVE,
                tenant=tenant,
                comments='First comment!',
            ),
            Location(
                name='Location 3',
                slug='location-3',
                site=site,
                status=LocationStatusChoices.STATUS_ACTIVE,
                tenant=tenant,
                comments='_This_ is a **bold comment**',
            ),
        )
        for location in locations:
            location.save()

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Location X',
            'slug': 'location-x',
            'site': site.pk,
            'status': LocationStatusChoices.STATUS_PLANNED,
            'facility': 'Facility X',
            'tenant': tenant.pk,
            'description': 'A new location',
            'tags': [t.pk for t in tags],
            'comments': 'This comment is really boring',
        }

        cls.csv_data = (
            "site,tenant,name,slug,status,description,comments",
            "Site 1,Tenant 1,Location 4,location-4,planned,Fourth location,",
            "Site 1,Tenant 1,Location 5,location-5,planned,Fifth location,",
            "Site 1,Tenant 1,Location 6,location-6,planned,Sixth location,hi!",
        )

        cls.csv_update_data = (
            "id,name,description,comments",
            f"{locations[0].pk},Location 7,Fourth location7,Useful comment",
            f"{locations[1].pk},Location 8,Fifth location8,unuseful comment",
            f"{locations[2].pk},Location 0,Sixth location9,",
        )

        cls.bulk_edit_data = {
            'description': 'New description',
            'comments': 'This comment is also really boring',
        }


class RackGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
    model = RackGroup

    @classmethod
    def setUpTestData(cls):

        rack_groups = (
            RackGroup(name='Rack Group 1', slug='rack-group-1'),
            RackGroup(name='Rack Group 2', slug='rack-group-2'),
            RackGroup(name='Rack Group 3', slug='rack-group-3'),
        )
        RackGroup.objects.bulk_create(rack_groups)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Rack Group X',
            'slug': 'rack-group-x',
            'description': 'New group',
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "name,slug,description",
            "Rack Group 4,rack-group-4,Fourth group",
            "Rack Group 5,rack-group-5,Fifth group",
            "Rack Group 6,rack-group-6,",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{rack_groups[0].pk},Rack Group 7,New description7",
            f"{rack_groups[1].pk},Rack Group 8,New description8",
            f"{rack_groups[2].pk},Rack Group 9,New description9",
        )

        cls.bulk_edit_data = {
            'description': 'New description',
        }


class RackRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
    model = RackRole

    @classmethod
    def setUpTestData(cls):

        rack_roles = (
            RackRole(name='Rack Role 1', slug='rack-role-1'),
            RackRole(name='Rack Role 2', slug='rack-role-2'),
            RackRole(name='Rack Role 3', slug='rack-role-3'),
        )
        RackRole.objects.bulk_create(rack_roles)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Rack Role X',
            'slug': 'rack-role-x',
            'color': 'c0c0c0',
            'description': 'New role',
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "name,slug,color",
            "Rack Role 4,rack-role-4,ff0000",
            "Rack Role 5,rack-role-5,00ff00",
            "Rack Role 6,rack-role-6,0000ff",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{rack_roles[0].pk},Rack Role 7,New description7",
            f"{rack_roles[1].pk},Rack Role 8,New description8",
            f"{rack_roles[2].pk},Rack Role 9,New description9",
        )

        cls.bulk_edit_data = {
            'color': '00ff00',
            'description': 'New description',
        }


class RackReservationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
    model = RackReservation

    @classmethod
    def setUpTestData(cls):

        user2 = User.objects.create_user(username='testuser2')
        user3 = User.objects.create_user(username='testuser3')

        site = Site.objects.create(name='Site 1', slug='site-1')

        location = Location(name='Location 1', slug='location-1', site=site)
        location.save()

        rack = Rack(name='Rack 1', site=site, location=location)
        rack.save()

        rack_reservations = (
            RackReservation(rack=rack, user=user2, units=[1, 2, 3], description='Reservation 1'),
            RackReservation(rack=rack, user=user2, units=[4, 5, 6], description='Reservation 2'),
            RackReservation(rack=rack, user=user2, units=[7, 8, 9], description='Reservation 3'),
        )
        RackReservation.objects.bulk_create(rack_reservations)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'rack': rack.pk,
            'units': "10,11,12",
            'status': RackReservationStatusChoices.STATUS_PENDING,
            'user': user3.pk,
            'tenant': None,
            'description': 'Rack reservation',
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            'site,location,rack,units,status,description',
            'Site 1,Location 1,Rack 1,"10,11,12",active,Reservation 1',
            'Site 1,Location 1,Rack 1,"13,14,15",pending,Reservation 2',
            'Site 1,Location 1,Rack 1,"16,17,18",stale,Reservation 3',
        )

        cls.csv_update_data = (
            'id,description',
            f'{rack_reservations[0].pk},New description1',
            f'{rack_reservations[1].pk},New description2',
            f'{rack_reservations[2].pk},New description3',
        )

        cls.bulk_edit_data = {
            'status': RackReservationStatusChoices.STATUS_STALE,
            'user': user3.pk,
            'tenant': None,
            'description': 'New description',
        }


class RackTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
    model = RackType

    @classmethod
    def setUpTestData(cls):
        manufacturers = (
            Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
            Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
        )
        Manufacturer.objects.bulk_create(manufacturers)

        rack_types = (
            RackType(
                manufacturer=manufacturers[0],
                model='RackType 1',
                slug='rack-type-1',
                form_factor=RackFormFactorChoices.TYPE_CABINET,
            ),
            RackType(
                manufacturer=manufacturers[0],
                model='RackType 2',
                slug='rack-type-2',
                form_factor=RackFormFactorChoices.TYPE_CABINET,
            ),
            RackType(
                manufacturer=manufacturers[0],
                model='RackType 3',
                slug='rack-type-3',
                form_factor=RackFormFactorChoices.TYPE_CABINET,
            ),
        )
        RackType.objects.bulk_create(rack_types)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'manufacturer': manufacturers[1].pk,
            'model': 'RackType X',
            'slug': 'rack-type-x',
            'type': RackFormFactorChoices.TYPE_CABINET,
            'width': RackWidthChoices.WIDTH_19IN,
            'u_height': 48,
            'desc_units': False,
            'outer_width': 500,
            'outer_depth': 500,
            'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER,
            'starting_unit': 1,
            'weight': 100,
            'max_weight': 2000,
            'weight_unit': WeightUnitChoices.UNIT_POUND,
            'form_factor': RackFormFactorChoices.TYPE_CABINET,
            'comments': 'Some comments',
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "manufacturer,model,slug,width,u_height,weight,max_weight,weight_unit",
            "Manufacturer 1,RackType 4,rack-type-4,19,42,100,2000,kg",
            "Manufacturer 1,RackType 5,rack-type-5,19,42,100,2000,kg",
            "Manufacturer 1,RackType 6,rack-type-6,19,42,100,2000,kg",
        )

        cls.csv_update_data = (
            "id,model",
            f"{rack_types[0].pk},RackType 7",
            f"{rack_types[1].pk},RackType 8",
            f"{rack_types[2].pk},RackType 9",
        )

        cls.bulk_edit_data = {
            'manufacturer': manufacturers[1].pk,
            'type': RackFormFactorChoices.TYPE_4POST,
            'width': RackWidthChoices.WIDTH_23IN,
            'u_height': 49,
            'desc_units': True,
            'outer_width': 30,
            'outer_depth': 30,
            'outer_unit': RackDimensionUnitChoices.UNIT_INCH,
            'weight': 200,
            'max_weight': 4000,
            'weight_unit': WeightUnitChoices.UNIT_POUND,
            'comments': 'New comments',
        }


class RackTestCase(ViewTestCases.PrimaryObjectViewTestCase):
    model = Rack

    @classmethod
    def setUpTestData(cls):

        sites = (
            Site(name='Site 1', slug='site-1'),
            Site(name='Site 2', slug='site-2'),
        )
        Site.objects.bulk_create(sites)

        locations = (
            Location(name='Location 1', slug='location-1', site=sites[0]),
            Location(name='Location 2', slug='location-2', site=sites[1])
        )
        for location in locations:
            location.save()

        rack_groups = (
            RackGroup(name='Rack Group 1', slug='rack-group-1'),
            RackGroup(name='Rack Group 2', slug='rack-group-2'),
        )
        RackGroup.objects.bulk_create(rack_groups)

        rackroles = (
            RackRole(name='Rack Role 1', slug='rack-role-1'),
            RackRole(name='Rack Role 2', slug='rack-role-2'),
        )
        RackRole.objects.bulk_create(rackroles)

        racks = (
            Rack(name='Rack 1', site=sites[0], group=rack_groups[0], role=rackroles[0]),
            Rack(name='Rack 2', site=sites[0], group=rack_groups[1]),
            Rack(name='Rack 3', site=sites[0]),
        )
        Rack.objects.bulk_create(racks)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Rack X',
            'facility_id': 'Facility X',
            'site': sites[1].pk,
            'location': locations[1].pk,
            'group': rack_groups[1].pk,
            'tenant': None,
            'status': RackStatusChoices.STATUS_PLANNED,
            'role': rackroles[1].pk,
            'serial': '123456',
            'asset_tag': 'ABCDEF',
            'form_factor': RackFormFactorChoices.TYPE_CABINET,
            'width': RackWidthChoices.WIDTH_19IN,
            'u_height': 48,
            'desc_units': False,
            'outer_width': 500,
            'outer_depth': 500,
            'outer_unit': RackDimensionUnitChoices.UNIT_MILLIMETER,
            'starting_unit': 1,
            'weight': 100,
            'max_weight': 2000,
            'weight_unit': WeightUnitChoices.UNIT_POUND,
            'comments': 'Some comments',
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "site,location,group,name,status,width,u_height,weight,max_weight,weight_unit",
            "Site 1,,,Rack 4,active,19,42,100,2000,kg",
            "Site 1,Location 1,Rack Group 1,Rack 5,active,19,42,100,2000,kg",
            "Site 2,Location 2,Rack Group 2,Rack 6,active,19,42,100,2000,kg",
        )

        cls.csv_update_data = (
            "id,name,status",
            f"{racks[0].pk},Rack 7,{RackStatusChoices.STATUS_DEPRECATED}",
            f"{racks[1].pk},Rack 8,{RackStatusChoices.STATUS_DEPRECATED}",
            f"{racks[2].pk},Rack 9,{RackStatusChoices.STATUS_DEPRECATED}",
        )

        cls.bulk_edit_data = {
            'site': sites[1].pk,
            'location': locations[1].pk,
            'group': rack_groups[1].pk,
            'tenant': None,
            'status': RackStatusChoices.STATUS_DEPRECATED,
            'role': rackroles[1].pk,
            'serial': '654321',
            'form_factor': RackFormFactorChoices.TYPE_4POST,
            'width': RackWidthChoices.WIDTH_23IN,
            'u_height': 49,
            'desc_units': True,
            'outer_width': 30,
            'outer_depth': 30,
            'outer_unit': RackDimensionUnitChoices.UNIT_INCH,
            'weight': 200,
            'max_weight': 4000,
            'weight_unit': WeightUnitChoices.UNIT_POUND,
            'comments': 'New comments',
        }

    def test_list_rack_elevations(self):
        """
        Test viewing the list of rack elevations.
        """
        self.add_permissions('dcim.view_rack')
        response = self.client.get(reverse('dcim:rack_elevation_list'))
        self.assertHttpStatus(response, 200)


class ManufacturerTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
    model = Manufacturer

    @classmethod
    def setUpTestData(cls):

        manufacturers = (
            Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
            Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
            Manufacturer(name='Manufacturer 3', slug='manufacturer-3'),
        )
        Manufacturer.objects.bulk_create(manufacturers)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Manufacturer X',
            'slug': 'manufacturer-x',
            'description': 'A new manufacturer',
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "name,slug,description",
            "Manufacturer 4,manufacturer-4,Fourth manufacturer",
            "Manufacturer 5,manufacturer-5,Fifth manufacturer",
            "Manufacturer 6,manufacturer-6,Sixth manufacturer",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{manufacturers[0].pk},Manufacturer 7,Fourth manufacturer7",
            f"{manufacturers[1].pk},Manufacturer 8,Fifth manufacturer8",
            f"{manufacturers[2].pk},Manufacturer 9,Sixth manufacturer9",
        )

        cls.bulk_edit_data = {
            'description': 'New description',
        }


# TODO: Change base class to PrimaryObjectViewTestCase
# Blocked by absence of bulk import view for DeviceTypes
class DeviceTypeTestCase(
    ViewTestCases.GetObjectViewTestCase,
    ViewTestCases.GetObjectChangelogViewTestCase,
    ViewTestCases.CreateObjectViewTestCase,
    ViewTestCases.EditObjectViewTestCase,
    ViewTestCases.DeleteObjectViewTestCase,
    ViewTestCases.ListObjectsViewTestCase,
    ViewTestCases.BulkEditObjectsViewTestCase,
    ViewTestCases.BulkDeleteObjectsViewTestCase
):
    model = DeviceType

    @classmethod
    def setUpTestData(cls):

        manufacturers = (
            Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
            Manufacturer(name='Manufacturer 2', slug='manufacturer-2')
        )
        Manufacturer.objects.bulk_create(manufacturers)

        platforms = (
            Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturers[0]),
            Platform(name='Platform 2', slug='platform-3', manufacturer=manufacturers[1]),
        )
        for platform in platforms:
            platform.save()

        DeviceType.objects.bulk_create([
            DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturers[0]),
            DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturers[0]),
            DeviceType(model='Device Type 3', slug='device-type-3', manufacturer=manufacturers[0]),
        ])

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'manufacturer': manufacturers[1].pk,
            'default_platform': platforms[0].pk,
            'model': 'Device Type X',
            'slug': 'device-type-x',
            'part_number': '123ABC',
            'u_height': 2,
            'is_full_depth': True,
            'subdevice_role': None,
            'comments': 'Some comments',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_edit_data = {
            'manufacturer': manufacturers[1].pk,
            'default_platform': platforms[1].pk,
            'u_height': 3,
            'is_full_depth': False,
        }

    def test_devicetype_consoleports(self):
        self.add_permissions('dcim.view_devicetype', 'dcim.view_consoleporttemplate')
        devicetype = DeviceType.objects.first()
        console_ports = (
            ConsolePortTemplate(device_type=devicetype, name='Console Port 1'),
            ConsolePortTemplate(device_type=devicetype, name='Console Port 2'),
            ConsolePortTemplate(device_type=devicetype, name='Console Port 3'),
        )
        ConsolePortTemplate.objects.bulk_create(console_ports)

        url = reverse('dcim:devicetype_consoleports', kwargs={'pk': devicetype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_devicetype_consoleserverports(self):
        self.add_permissions('dcim.view_devicetype', 'dcim.view_consoleserverporttemplate')
        devicetype = DeviceType.objects.first()
        console_server_ports = (
            ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 1'),
            ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 2'),
            ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port 3'),
        )
        ConsoleServerPortTemplate.objects.bulk_create(console_server_ports)

        url = reverse('dcim:devicetype_consoleserverports', kwargs={'pk': devicetype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_devicetype_powerports(self):
        self.add_permissions('dcim.view_devicetype', 'dcim.view_powerporttemplate')
        devicetype = DeviceType.objects.first()
        power_ports = (
            PowerPortTemplate(device_type=devicetype, name='Power Port 1'),
            PowerPortTemplate(device_type=devicetype, name='Power Port 2'),
            PowerPortTemplate(device_type=devicetype, name='Power Port 3'),
        )
        PowerPortTemplate.objects.bulk_create(power_ports)

        url = reverse('dcim:devicetype_powerports', kwargs={'pk': devicetype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_devicetype_poweroutlets(self):
        self.add_permissions('dcim.view_devicetype', 'dcim.view_poweroutlettemplate')
        devicetype = DeviceType.objects.first()
        power_outlets = (
            PowerOutletTemplate(device_type=devicetype, name='Power Outlet 1'),
            PowerOutletTemplate(device_type=devicetype, name='Power Outlet 2'),
            PowerOutletTemplate(device_type=devicetype, name='Power Outlet 3'),
        )
        PowerOutletTemplate.objects.bulk_create(power_outlets)

        url = reverse('dcim:devicetype_poweroutlets', kwargs={'pk': devicetype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_devicetype_interfaces(self):
        self.add_permissions('dcim.view_devicetype', 'dcim.view_interfacetemplate')
        devicetype = DeviceType.objects.first()
        interfaces = (
            InterfaceTemplate(device_type=devicetype, name='Interface 1'),
            InterfaceTemplate(device_type=devicetype, name='Interface 2'),
            InterfaceTemplate(device_type=devicetype, name='Interface 3'),
        )
        InterfaceTemplate.objects.bulk_create(interfaces)

        url = reverse('dcim:devicetype_interfaces', kwargs={'pk': devicetype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_devicetype_rearports(self):
        self.add_permissions('dcim.view_devicetype', 'dcim.view_rearporttemplate')
        devicetype = DeviceType.objects.first()
        rear_ports = (
            RearPortTemplate(device_type=devicetype, name='Rear Port 1'),
            RearPortTemplate(device_type=devicetype, name='Rear Port 2'),
            RearPortTemplate(device_type=devicetype, name='Rear Port 3'),
        )
        RearPortTemplate.objects.bulk_create(rear_ports)

        url = reverse('dcim:devicetype_rearports', kwargs={'pk': devicetype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_devicetype_frontports(self):
        self.add_permissions(
            'dcim.view_devicetype',
            'dcim.view_frontporttemplate',
            'dcim.view_rearporttemplate',
        )
        devicetype = DeviceType.objects.first()
        rear_ports = (
            RearPortTemplate(device_type=devicetype, name='Rear Port 1'),
            RearPortTemplate(device_type=devicetype, name='Rear Port 2'),
            RearPortTemplate(device_type=devicetype, name='Rear Port 3'),
        )
        RearPortTemplate.objects.bulk_create(rear_ports)
        front_ports = (
            FrontPortTemplate(device_type=devicetype, name='Front Port 1'),
            FrontPortTemplate(device_type=devicetype, name='Front Port 2'),
            FrontPortTemplate(device_type=devicetype, name='Front Port 3'),
        )
        FrontPortTemplate.objects.bulk_create(front_ports)
        PortTemplateMapping.objects.bulk_create([
            PortTemplateMapping(device_type=devicetype, front_port=front_ports[0], rear_port=rear_ports[0]),
            PortTemplateMapping(device_type=devicetype, front_port=front_ports[1], rear_port=rear_ports[1]),
            PortTemplateMapping(device_type=devicetype, front_port=front_ports[2], rear_port=rear_ports[2]),
        ])

        url = reverse('dcim:devicetype_frontports', kwargs={'pk': devicetype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_devicetype_modulebays(self):
        self.add_permissions('dcim.view_devicetype', 'dcim.view_modulebaytemplate')
        devicetype = DeviceType.objects.first()
        module_bays = (
            ModuleBayTemplate(device_type=devicetype, name='Module Bay 1'),
            ModuleBayTemplate(device_type=devicetype, name='Module Bay 2'),
            ModuleBayTemplate(device_type=devicetype, name='Module Bay 3'),
        )
        ModuleBayTemplate.objects.bulk_create(module_bays)

        url = reverse('dcim:devicetype_modulebays', kwargs={'pk': devicetype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_devicetype_devicebays(self):
        self.add_permissions('dcim.view_devicetype', 'dcim.view_devicebaytemplate')
        devicetype = DeviceType.objects.first()
        device_bays = (
            DeviceBayTemplate(device_type=devicetype, name='Device Bay 1'),
            DeviceBayTemplate(device_type=devicetype, name='Device Bay 2'),
            DeviceBayTemplate(device_type=devicetype, name='Device Bay 3'),
        )
        DeviceBayTemplate.objects.bulk_create(device_bays)

        url = reverse('dcim:devicetype_devicebays', kwargs={'pk': devicetype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_devicetype_inventoryitems(self):
        self.add_permissions('dcim.view_devicetype', 'dcim.view_inventoryitemtemplate')
        devicetype = DeviceType.objects.first()
        inventory_items = (
            DeviceBayTemplate(device_type=devicetype, name='Device Bay 1'),
            DeviceBayTemplate(device_type=devicetype, name='Device Bay 2'),
            DeviceBayTemplate(device_type=devicetype, name='Device Bay 3'),
        )
        for inventory_item in inventory_items:
            inventory_item.save()

        url = reverse('dcim:devicetype_inventoryitems', kwargs={'pk': devicetype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_import_objects(self):
        """
        Custom import test for YAML-based imports (versus CSV)
        """
        self.add_permissions('dcim.view_manufacturer', 'dcim.view_platform')
        IMPORT_DATA = """
manufacturer: Generic
model: TEST-1000
slug: test-1000
default_platform: Platform
u_height: 2
is_full_depth: false
airflow: front-to-rear
subdevice_role: parent
weight: 10
weight_unit: kg
comments: Test comment
console-ports:
  - name: Console Port 1
    type: de-9
  - name: Console Port 2
    type: de-9
  - name: Console Port 3
    type: de-9
console-server-ports:
  - name: Console Server Port 1
    type: rj-45
  - name: Console Server Port 2
    type: rj-45
  - name: Console Server Port 3
    type: rj-45
power-ports:
  - name: Power Port 1
    type: iec-60320-c14
  - name: Power Port 2
    type: iec-60320-c14
  - name: Power Port 3
    type: iec-60320-c14
power-outlets:
  - name: Power Outlet 1
    type: iec-60320-c13
    power_port: Power Port 1
    feed_leg: A
  - name: Power Outlet 2
    type: iec-60320-c13
    power_port: Power Port 1
    feed_leg: A
  - name: Power Outlet 3
    type: iec-60320-c13
    power_port: Power Port 1
    feed_leg: A
interfaces:
  - name: Interface 1
    type: 1000base-t
    mgmt_only: true
  - name: Interface 2
    type: 1000base-t
    enabled: false
  - name: Interface 3
    type: 1000base-t
rear-ports:
  - name: Rear Port 1
    type: 8p8c
  - name: Rear Port 2
    type: 8p8c
  - name: Rear Port 3
    type: 8p8c
front-ports:
  - name: Front Port 1
    type: 8p8c
  - name: Front Port 2
    type: 8p8c
  - name: Front Port 3
    type: 8p8c
port-mappings:
  - front_port: Front Port 1
    rear_port: Rear Port 1
  - front_port: Front Port 2
    rear_port: Rear Port 2
  - front_port: Front Port 3
    rear_port: Rear Port 3
module-bays:
  - name: Module Bay 1
  - name: Module Bay 2
  - name: Module Bay 3
device-bays:
  - name: Device Bay 1
  - name: Device Bay 2
  - name: Device Bay 3
inventory-items:
  - name: Inventory Item 1
    manufacturer: Generic
  - name: Inventory Item 2
    manufacturer: Generic
  - name: Inventory Item 3
    manufacturer: Generic
"""

        # Create the manufacturer and platform
        manufacturer = Manufacturer(name='Generic', slug='generic')
        manufacturer.save()
        platform = Platform(name='Platform', slug='test-platform', manufacturer=manufacturer)
        platform.save()

        # Add all required permissions to the test user
        self.add_permissions(
            'dcim.view_devicetype',
            'dcim.add_devicetype',
            'dcim.add_consoleporttemplate',
            'dcim.add_consoleserverporttemplate',
            'dcim.add_powerporttemplate',
            'dcim.add_poweroutlettemplate',
            'dcim.add_interfacetemplate',
            'dcim.add_frontporttemplate',
            'dcim.add_rearporttemplate',
            'dcim.add_modulebaytemplate',
            'dcim.add_devicebaytemplate',
            'dcim.add_inventoryitemtemplate',
        )

        form_data = {
            'data': IMPORT_DATA,
            'format': 'yaml'
        }
        response = self.client.post(reverse('dcim:devicetype_bulk_import'), data=form_data, follow=True)
        self.assertHttpStatus(response, 200)

        device_type = DeviceType.objects.get(model='TEST-1000')
        self.assertEqual(device_type.manufacturer.pk, manufacturer.pk)
        self.assertEqual(device_type.default_platform.pk, platform.pk)
        self.assertEqual(device_type.slug, 'test-1000')
        self.assertEqual(device_type.u_height, 2)
        self.assertFalse(device_type.is_full_depth)
        self.assertEqual(device_type.airflow, DeviceAirflowChoices.AIRFLOW_FRONT_TO_REAR)
        self.assertEqual(device_type.subdevice_role, SubdeviceRoleChoices.ROLE_PARENT)
        self.assertEqual(device_type.weight, 10)
        self.assertEqual(device_type.weight_unit, WeightUnitChoices.UNIT_KILOGRAM)
        self.assertEqual(device_type.comments, 'Test comment')

        # Verify all of the components were created
        self.assertEqual(device_type.consoleporttemplates.count(), 3)
        cp1 = ConsolePortTemplate.objects.first()
        self.assertEqual(cp1.name, 'Console Port 1')
        self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)

        self.assertEqual(device_type.consoleserverporttemplates.count(), 3)
        csp1 = ConsoleServerPortTemplate.objects.first()
        self.assertEqual(csp1.name, 'Console Server Port 1')
        self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)

        self.assertEqual(device_type.powerporttemplates.count(), 3)
        pp1 = PowerPortTemplate.objects.first()
        self.assertEqual(pp1.name, 'Power Port 1')
        self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)

        self.assertEqual(device_type.poweroutlettemplates.count(), 3)
        po1 = PowerOutletTemplate.objects.first()
        self.assertEqual(po1.name, 'Power Outlet 1')
        self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
        self.assertEqual(po1.power_port, pp1)
        self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)

        self.assertEqual(device_type.interfacetemplates.count(), 3)
        iface1 = InterfaceTemplate.objects.first()
        self.assertEqual(iface1.name, 'Interface 1')
        self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
        self.assertTrue(iface1.mgmt_only)
        self.assertTrue(iface1.enabled)

        iface2 = InterfaceTemplate.objects.filter(name="Interface 2").first()
        self.assertFalse(iface2.enabled)

        self.assertEqual(device_type.rearporttemplates.count(), 3)
        rp1 = RearPortTemplate.objects.first()
        self.assertEqual(rp1.name, 'Rear Port 1')

        self.assertEqual(device_type.frontporttemplates.count(), 3)
        fp1 = FrontPortTemplate.objects.first()
        self.assertEqual(fp1.name, 'Front Port 1')

        self.assertEqual(device_type.port_mappings.count(), 3)
        mapping1 = PortTemplateMapping.objects.first()
        self.assertEqual(mapping1.device_type, device_type)
        self.assertEqual(mapping1.front_port, fp1)
        self.assertEqual(mapping1.rear_port, rp1)

        self.assertEqual(device_type.modulebaytemplates.count(), 3)
        mb1 = ModuleBayTemplate.objects.first()
        self.assertEqual(mb1.name, 'Module Bay 1')

        self.assertEqual(device_type.devicebaytemplates.count(), 3)
        db1 = DeviceBayTemplate.objects.first()
        self.assertEqual(db1.name, 'Device Bay 1')

        self.assertEqual(device_type.inventoryitemtemplates.count(), 3)
        ii1 = InventoryItemTemplate.objects.first()
        self.assertEqual(ii1.name, 'Inventory Item 1')

    def test_import_error_numbering(self):
        # Add all required permissions to the test user
        self.add_permissions(
            'dcim.view_devicetype',
            'dcim.add_devicetype',
            'dcim.view_manufacturer',
            'dcim.add_consoleporttemplate',
            'dcim.add_consoleserverporttemplate',
            'dcim.add_powerporttemplate',
            'dcim.add_poweroutlettemplate',
            'dcim.add_interfacetemplate',
            'dcim.add_frontporttemplate',
            'dcim.add_rearporttemplate',
            'dcim.add_modulebaytemplate',
            'dcim.add_devicebaytemplate',
            'dcim.add_inventoryitemtemplate',
        )

        import_data = '''
---
manufacturer: Manufacturer 1
model: TEST-2001
slug: test-2001
u_height: 1
module-bays:
  - name: Module Bay 1-1
  - name: Module Bay 1-2
---
- manufacturer: Manufacturer 1
  model: TEST-2002
  slug: test-2002
  u_height: 1
  module-bays:
    - name: Module Bay 2-1
    - name: Module Bay 2-2
    - not_name: Module Bay 2-3
- manufacturer: Manufacturer 1
  model: TEST-2003
  slug: test-2003
  u_height: 1
  module-bays:
    - name: Module Bay 3-1
'''
        form_data = {
            'data': import_data,
            'format': 'yaml'
        }

        response = self.client.post(reverse('dcim:devicetype_bulk_import'), data=form_data, follow=True)
        self.assertHttpStatus(response, 200)
        self.assertContains(response, "Record 2 module-bays[3].name: This field is required.")

    def test_import_nolist(self):
        # Add all required permissions to the test user
        self.add_permissions(
            'dcim.view_devicetype',
            'dcim.add_devicetype',
            'dcim.view_manufacturer',
            'dcim.add_consoleporttemplate',
            'dcim.add_consoleserverporttemplate',
            'dcim.add_powerporttemplate',
            'dcim.add_poweroutlettemplate',
            'dcim.add_interfacetemplate',
            'dcim.add_frontporttemplate',
            'dcim.add_rearporttemplate',
            'dcim.add_modulebaytemplate',
            'dcim.add_devicebaytemplate',
            'dcim.add_inventoryitemtemplate',
        )

        for value in ('', 'null', '3', '"My console port"', '{name: "My other console port"}'):
            with self.subTest(value=value):
                import_data = f'''
manufacturer: Manufacturer 1
model: TEST-3000
slug: test-3000
u_height: 1
console-ports: {value}
'''
                form_data = {
                    'data': import_data,
                    'format': 'yaml'
                }

                response = self.client.post(reverse('dcim:devicetype_bulk_import'), data=form_data, follow=True)
                self.assertHttpStatus(response, 200)
                self.assertContains(response, "Record 1 console-ports: Must be a list.")

    def test_import_nodict(self):
        # Add all required permissions to the test user
        self.add_permissions(
            'dcim.view_devicetype',
            'dcim.add_devicetype',
            'dcim.view_manufacturer',
            'dcim.add_consoleporttemplate',
            'dcim.add_consoleserverporttemplate',
            'dcim.add_powerporttemplate',
            'dcim.add_poweroutlettemplate',
            'dcim.add_interfacetemplate',
            'dcim.add_frontporttemplate',
            'dcim.add_rearporttemplate',
            'dcim.add_modulebaytemplate',
            'dcim.add_devicebaytemplate',
            'dcim.add_inventoryitemtemplate',
        )

        for value in ('', 'null', '3', '"My console port"', '["My other console port"]'):
            with self.subTest(value=value):
                import_data = f'''
manufacturer: Manufacturer 1
model: TEST-4000
slug: test-4000
u_height: 1
console-ports:
  - {value}
'''
                form_data = {
                    'data': import_data,
                    'format': 'yaml'
                }

                response = self.client.post(reverse('dcim:devicetype_bulk_import'), data=form_data, follow=True)
                self.assertHttpStatus(response, 200)
                self.assertContains(response, "Record 1 console-ports[1]: Must be a dictionary.")

    @override_settings(STREAMING_EXPORTS=True)
    def test_export_objects(self):
        url = reverse('dcim:devicetype_list')
        self.add_permissions('dcim.view_devicetype')

        # Test default YAML export
        response = self.client.get(f'{url}?export')
        self.assertEqual(response.status_code, 200)
        data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader))
        self.assertEqual(len(data), 3)
        self.assertEqual(data[0]['manufacturer'], 'Manufacturer 1')
        self.assertEqual(data[0]['model'], 'Device Type 1')

        # Test table-based export (streams row-by-row)
        response = self.client.get(f'{url}?export=table')
        self.assertHttpStatus(response, 200)
        self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
        self.assertIsInstance(response, StreamingHttpResponse)
        content = b''.join(response.streaming_content).decode('utf-8')
        rows = list(csv.reader(StringIO(content)))
        self.assertGreater(len(rows), 1)
        self.assertEqual(len(rows) - 1, DeviceType.objects.count())


class ModuleTypeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
    model = ModuleType

    SCHEMA = {
        'properties': {
            'media': {
                'title': 'Media',
                'type': 'array',
                'items': {
                    'type': 'string',
                    'enum': ['copper', 'sfp', 'qsfp28'],
                },
            },
        },
    }

    @classmethod
    def setUpTestData(cls):

        manufacturers = (
            Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
            Manufacturer(name='Manufacturer 2', slug='manufacturer-2')
        )
        Manufacturer.objects.bulk_create(manufacturers)

        profile = ModuleTypeProfile.objects.create(name='Module Type Profile 1', schema=cls.SCHEMA)

        module_types = ModuleType.objects.bulk_create([
            ModuleType(
                model='Module Type 1',
                manufacturer=manufacturers[0],
                profile=profile,
                attribute_data={'media': ['copper', 'qsfp28']},
            ),
            ModuleType(model='Module Type 2', manufacturer=manufacturers[0]),
            ModuleType(model='Module Type 3', manufacturer=manufacturers[0]),
        ])

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        fan_module_type_profile = ModuleTypeProfile.objects.get(name='Fan')

        cls.form_data = {
            'manufacturer': manufacturers[1].pk,
            'model': 'Device Type X',
            'part_number': '123ABC',
            'comments': 'Some comments',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_edit_data = {
            'manufacturer': manufacturers[1].pk,
            'part_number': '456DEF',
        }

        cls.csv_data = (
            "manufacturer,model,part_number,comments,profile",
            f"Manufacturer 1,fan0,generic-fan,,{fan_module_type_profile.name}"
        )

        cls.csv_update_data = (
            "id,model",
            f"{module_types[0].id},test model",
        )

    def test_bulk_update_objects_with_permission(self):
        self.add_permissions(
            'dcim.add_consoleporttemplate',
            'dcim.add_consoleserverporttemplate',
            'dcim.add_powerporttemplate',
            'dcim.add_poweroutlettemplate',
            'dcim.add_interfacetemplate',
            'dcim.add_frontporttemplate',
            'dcim.add_rearporttemplate',
            'dcim.add_modulebaytemplate',
        )

        # run base test
        super().test_bulk_update_objects_with_permission()

    def test_bulk_update_objects_without_change_permission(self):
        # ModuleTypeImportView declares these as additional_permissions, so they're required to reach the view
        self.add_permissions(
            'dcim.add_consoleporttemplate',
            'dcim.add_consoleserverporttemplate',
            'dcim.add_powerporttemplate',
            'dcim.add_poweroutlettemplate',
            'dcim.add_interfacetemplate',
            'dcim.add_frontporttemplate',
            'dcim.add_rearporttemplate',
            'dcim.add_modulebaytemplate',
        )

        # run base test
        super().test_bulk_update_objects_without_change_permission()

    @tag('regression')
    def test_bulk_import_objects_with_permission(self):
        self.add_permissions(
            'dcim.add_consoleporttemplate',
            'dcim.add_consoleserverporttemplate',
            'dcim.add_powerporttemplate',
            'dcim.add_poweroutlettemplate',
            'dcim.add_interfacetemplate',
            'dcim.add_frontporttemplate',
            'dcim.add_rearporttemplate',
            'dcim.add_modulebaytemplate',
        )

        def verify_module_type_profile(scenario_name):
            # TODO: remove extra regression asserts once parent test supports testing all import fields
            fan_module_type = ModuleType.objects.get(part_number='generic-fan')
            fan_module_type_profile = ModuleTypeProfile.objects.get(name='Fan')
            assert fan_module_type.profile == fan_module_type_profile

        # run base test
        super().test_bulk_import_objects_with_permission(post_import_callback=verify_module_type_profile)

    def test_bulk_import_objects_with_constrained_permission(self):
        self.add_permissions(
            'dcim.add_consoleporttemplate',
            'dcim.add_consoleserverporttemplate',
            'dcim.add_powerporttemplate',
            'dcim.add_poweroutlettemplate',
            'dcim.add_interfacetemplate',
            'dcim.add_frontporttemplate',
            'dcim.add_rearporttemplate',
            'dcim.add_modulebaytemplate',
        )

        super().test_bulk_import_objects_with_constrained_permission()

    @tag('regression')
    def test_get_object_renders_profile_attribute_lists(self):
        self.add_permissions(
            'dcim.view_moduletype',
            'dcim.view_moduletypeprofile',
        )
        moduletype = ModuleType.objects.first()
        response = self.client.get(moduletype.get_absolute_url())

        self.assertHttpStatus(response, 200)
        self.assertContains(response, 'Media')
        self.assertContains(response, 'copper, qsfp28')

    def test_moduletype_consoleports(self):
        self.add_permissions('dcim.view_moduletype', 'dcim.view_consoleporttemplate')
        moduletype = ModuleType.objects.first()
        console_ports = (
            ConsolePortTemplate(module_type=moduletype, name='Console Port 1'),
            ConsolePortTemplate(module_type=moduletype, name='Console Port 2'),
            ConsolePortTemplate(module_type=moduletype, name='Console Port 3'),
        )
        ConsolePortTemplate.objects.bulk_create(console_ports)

        url = reverse('dcim:moduletype_consoleports', kwargs={'pk': moduletype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_moduletype_consoleserverports(self):
        self.add_permissions('dcim.view_moduletype', 'dcim.view_consoleserverporttemplate')
        moduletype = ModuleType.objects.first()
        console_server_ports = (
            ConsoleServerPortTemplate(module_type=moduletype, name='Console Server Port 1'),
            ConsoleServerPortTemplate(module_type=moduletype, name='Console Server Port 2'),
            ConsoleServerPortTemplate(module_type=moduletype, name='Console Server Port 3'),
        )
        ConsoleServerPortTemplate.objects.bulk_create(console_server_ports)

        url = reverse('dcim:moduletype_consoleserverports', kwargs={'pk': moduletype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_moduletype_powerports(self):
        self.add_permissions('dcim.view_moduletype', 'dcim.view_powerporttemplate')
        moduletype = ModuleType.objects.first()
        power_ports = (
            PowerPortTemplate(module_type=moduletype, name='Power Port 1'),
            PowerPortTemplate(module_type=moduletype, name='Power Port 2'),
            PowerPortTemplate(module_type=moduletype, name='Power Port 3'),
        )
        PowerPortTemplate.objects.bulk_create(power_ports)

        url = reverse('dcim:moduletype_powerports', kwargs={'pk': moduletype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_moduletype_poweroutlets(self):
        self.add_permissions('dcim.view_moduletype', 'dcim.view_poweroutlettemplate')
        moduletype = ModuleType.objects.first()
        power_outlets = (
            PowerOutletTemplate(module_type=moduletype, name='Power Outlet 1'),
            PowerOutletTemplate(module_type=moduletype, name='Power Outlet 2'),
            PowerOutletTemplate(module_type=moduletype, name='Power Outlet 3'),
        )
        PowerOutletTemplate.objects.bulk_create(power_outlets)

        url = reverse('dcim:moduletype_poweroutlets', kwargs={'pk': moduletype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_moduletype_interfaces(self):
        self.add_permissions('dcim.view_moduletype', 'dcim.view_interfacetemplate')
        moduletype = ModuleType.objects.first()
        interfaces = (
            InterfaceTemplate(module_type=moduletype, name='Interface 1'),
            InterfaceTemplate(module_type=moduletype, name='Interface 2'),
            InterfaceTemplate(module_type=moduletype, name='Interface 3'),
        )
        InterfaceTemplate.objects.bulk_create(interfaces)

        url = reverse('dcim:moduletype_interfaces', kwargs={'pk': moduletype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_moduletype_rearports(self):
        self.add_permissions('dcim.view_moduletype', 'dcim.view_rearporttemplate')
        moduletype = ModuleType.objects.first()
        rear_ports = (
            RearPortTemplate(module_type=moduletype, name='Rear Port 1'),
            RearPortTemplate(module_type=moduletype, name='Rear Port 2'),
            RearPortTemplate(module_type=moduletype, name='Rear Port 3'),
        )
        RearPortTemplate.objects.bulk_create(rear_ports)

        url = reverse('dcim:moduletype_rearports', kwargs={'pk': moduletype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_moduletype_frontports(self):
        self.add_permissions(
            'dcim.view_moduletype',
            'dcim.view_frontporttemplate',
            'dcim.view_rearporttemplate',
        )
        moduletype = ModuleType.objects.first()
        rear_ports = (
            RearPortTemplate(module_type=moduletype, name='Rear Port 1'),
            RearPortTemplate(module_type=moduletype, name='Rear Port 2'),
            RearPortTemplate(module_type=moduletype, name='Rear Port 3'),
        )
        RearPortTemplate.objects.bulk_create(rear_ports)
        front_ports = (
            FrontPortTemplate(module_type=moduletype, name='Front Port 1'),
            FrontPortTemplate(module_type=moduletype, name='Front Port 2'),
            FrontPortTemplate(module_type=moduletype, name='Front Port 3'),
        )
        FrontPortTemplate.objects.bulk_create(front_ports)
        PortTemplateMapping.objects.bulk_create([
            PortTemplateMapping(module_type=moduletype, front_port=front_ports[0], rear_port=rear_ports[0]),
            PortTemplateMapping(module_type=moduletype, front_port=front_ports[1], rear_port=rear_ports[1]),
            PortTemplateMapping(module_type=moduletype, front_port=front_ports[2], rear_port=rear_ports[2]),
        ])

        url = reverse('dcim:moduletype_frontports', kwargs={'pk': moduletype.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_import_objects(self):
        """
        Custom import test for YAML-based imports (versus CSV)
        """
        self.add_permissions('dcim.view_manufacturer')
        IMPORT_DATA = """
manufacturer: Generic
model: TEST-1000
weight: 10
weight_unit: lb
comments: Test comment
console-ports:
  - name: Console Port 1
    type: de-9
  - name: Console Port 2
    type: de-9
  - name: Console Port 3
    type: de-9
console-server-ports:
  - name: Console Server Port 1
    type: rj-45
  - name: Console Server Port 2
    type: rj-45
  - name: Console Server Port 3
    type: rj-45
power-ports:
  - name: Power Port 1
    type: iec-60320-c14
  - name: Power Port 2
    type: iec-60320-c14
  - name: Power Port 3
    type: iec-60320-c14
power-outlets:
  - name: Power Outlet 1
    type: iec-60320-c13
    power_port: Power Port 1
    feed_leg: A
  - name: Power Outlet 2
    type: iec-60320-c13
    power_port: Power Port 1
    feed_leg: A
  - name: Power Outlet 3
    type: iec-60320-c13
    power_port: Power Port 1
    feed_leg: A
interfaces:
  - name: Interface 1
    type: 1000base-t
    mgmt_only: true
  - name: Interface 2
    type: 1000base-t
  - name: Interface 3
    type: 1000base-t
rear-ports:
  - name: Rear Port 1
    type: 8p8c
  - name: Rear Port 2
    type: 8p8c
  - name: Rear Port 3
    type: 8p8c
front-ports:
  - name: Front Port 1
    type: 8p8c
  - name: Front Port 2
    type: 8p8c
  - name: Front Port 3
    type: 8p8c
port-mappings:
  - front_port: Front Port 1
    rear_port: Rear Port 1
  - front_port: Front Port 2
    rear_port: Rear Port 2
  - front_port: Front Port 3
    rear_port: Rear Port 3
module-bays:
  - name: Module Bay 1
    position: 1
  - name: Module Bay 2
    position: 2
  - name: Module Bay 3
    position: 3
"""

        # Create the manufacturer
        manufacturer = Manufacturer(name='Generic', slug='generic')
        manufacturer.save()

        # Add all required permissions to the test user
        self.add_permissions(
            'dcim.view_moduletype',
            'dcim.add_moduletype',
            'dcim.add_consoleporttemplate',
            'dcim.add_consoleserverporttemplate',
            'dcim.add_powerporttemplate',
            'dcim.add_poweroutlettemplate',
            'dcim.add_interfacetemplate',
            'dcim.add_frontporttemplate',
            'dcim.add_rearporttemplate',
            'dcim.add_modulebaytemplate',
        )

        form_data = {
            'data': IMPORT_DATA,
            'format': 'yaml'
        }
        response = self.client.post(reverse('dcim:moduletype_bulk_import'), data=form_data, follow=True)
        self.assertHttpStatus(response, 200)

        module_type = ModuleType.objects.get(model='TEST-1000')
        self.assertEqual(module_type.manufacturer.pk, manufacturer.pk)
        self.assertEqual(module_type.weight, 10)
        self.assertEqual(module_type.weight_unit, WeightUnitChoices.UNIT_POUND)
        self.assertEqual(module_type.comments, 'Test comment')

        # Verify all the components were created
        self.assertEqual(module_type.consoleporttemplates.count(), 3)
        cp1 = ConsolePortTemplate.objects.first()
        self.assertEqual(cp1.name, 'Console Port 1')
        self.assertEqual(cp1.type, ConsolePortTypeChoices.TYPE_DE9)

        self.assertEqual(module_type.consoleserverporttemplates.count(), 3)
        csp1 = ConsoleServerPortTemplate.objects.first()
        self.assertEqual(csp1.name, 'Console Server Port 1')
        self.assertEqual(csp1.type, ConsolePortTypeChoices.TYPE_RJ45)

        self.assertEqual(module_type.powerporttemplates.count(), 3)
        pp1 = PowerPortTemplate.objects.first()
        self.assertEqual(pp1.name, 'Power Port 1')
        self.assertEqual(pp1.type, PowerPortTypeChoices.TYPE_IEC_C14)

        self.assertEqual(module_type.poweroutlettemplates.count(), 3)
        po1 = PowerOutletTemplate.objects.first()
        self.assertEqual(po1.name, 'Power Outlet 1')
        self.assertEqual(po1.type, PowerOutletTypeChoices.TYPE_IEC_C13)
        self.assertEqual(po1.power_port, pp1)
        self.assertEqual(po1.feed_leg, PowerOutletFeedLegChoices.FEED_LEG_A)

        self.assertEqual(module_type.interfacetemplates.count(), 3)
        iface1 = InterfaceTemplate.objects.first()
        self.assertEqual(iface1.name, 'Interface 1')
        self.assertEqual(iface1.type, InterfaceTypeChoices.TYPE_1GE_FIXED)
        self.assertTrue(iface1.mgmt_only)

        self.assertEqual(module_type.rearporttemplates.count(), 3)
        rp1 = RearPortTemplate.objects.first()
        self.assertEqual(rp1.name, 'Rear Port 1')

        self.assertEqual(module_type.frontporttemplates.count(), 3)
        fp1 = FrontPortTemplate.objects.first()
        self.assertEqual(fp1.name, 'Front Port 1')

        self.assertEqual(module_type.port_mappings.count(), 3)
        mapping1 = PortTemplateMapping.objects.first()
        self.assertEqual(mapping1.module_type, module_type)
        self.assertEqual(mapping1.front_port, fp1)
        self.assertEqual(mapping1.rear_port, rp1)

        self.assertEqual(module_type.modulebaytemplates.count(), 3)
        mb1 = ModuleBayTemplate.objects.first()
        self.assertEqual(mb1.name, 'Module Bay 1')
        self.assertEqual(mb1.position, '1')

    @override_settings(STREAMING_EXPORTS=True)
    def test_export_objects(self):
        url = reverse('dcim:moduletype_list')
        self.add_permissions('dcim.view_moduletype')

        # Test default YAML export
        response = self.client.get(f'{url}?export')
        self.assertEqual(response.status_code, 200)
        data = list(yaml.load_all(response.content, Loader=yaml.SafeLoader))
        self.assertEqual(len(data), 3)
        self.assertEqual(data[0]['manufacturer'], 'Manufacturer 1')
        self.assertEqual(data[0]['model'], 'Module Type 1')

        # Test table-based export (streams row-by-row)
        response = self.client.get(f'{url}?export=table')
        self.assertHttpStatus(response, 200)
        self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
        self.assertIsInstance(response, StreamingHttpResponse)
        content = b''.join(response.streaming_content).decode('utf-8')
        rows = list(csv.reader(StringIO(content)))
        self.assertGreater(len(rows), 1)
        self.assertEqual(len(rows) - 1, ModuleType.objects.count())


class ModuleTypeProfileTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
    model = ModuleTypeProfile

    SCHEMAS = [
        {
            "properties": {
                "foo": {
                    "type": "string"
                }
            }
        },
        {
            "properties": {
                "foo": {
                    "type": "integer"
                }
            }
        },
        {
            "properties": {
                "foo": {
                    "type": "boolean"
                }
            }
        },
    ]

    @classmethod
    def setUpTestData(cls):
        module_type_profiles = (
            ModuleTypeProfile(
                name='Module Type Profile 1',
                schema=cls.SCHEMAS[0]
            ),
            ModuleTypeProfile(
                name='Module Type Profile 2',
                schema=cls.SCHEMAS[1]
            ),
            ModuleTypeProfile(
                name='Module Type Profile 3',
                schema=cls.SCHEMAS[2]
            ),
        )
        ModuleTypeProfile.objects.bulk_create(module_type_profiles)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Module Type Profile X',
            'description': 'A new profile',
            'schema': json.dumps(cls.SCHEMAS[0]),
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "name,schema",
            f"Module Type Profile 4,{json.dumps(cls.SCHEMAS[0])}",
            f"Module Type Profile 5,{json.dumps(cls.SCHEMAS[1])}",
            f"Module Type Profile 6,{json.dumps(cls.SCHEMAS[2])}",
        )

        cls.csv_update_data = (
            "id,description",
            f"{module_type_profiles[0].pk},New description",
            f"{module_type_profiles[1].pk},New description",
            f"{module_type_profiles[2].pk},New description",
        )

        cls.bulk_edit_data = {
            'description': 'New description',
        }


#
# DeviceType components
#

class ConsolePortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
    model = ConsolePortTemplate
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')

        ConsolePortTemplate.objects.bulk_create((
            ConsolePortTemplate(device_type=devicetype, name='Console Port Template 1'),
            ConsolePortTemplate(device_type=devicetype, name='Console Port Template 2'),
            ConsolePortTemplate(device_type=devicetype, name='Console Port Template 3'),
        ))

        cls.form_data = {
            'device_type': devicetype.pk,
            'name': 'Console Port Template X',
            'type': ConsolePortTypeChoices.TYPE_RJ45,
        }

        cls.bulk_create_data = {
            'device_type': devicetype.pk,
            'name': 'Console Port Template [4-6]',
            'type': ConsolePortTypeChoices.TYPE_RJ45,
        }

        cls.bulk_edit_data = {
            'type': ConsolePortTypeChoices.TYPE_RJ45,
        }


class ConsoleServerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
    model = ConsoleServerPortTemplate
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')

        ConsoleServerPortTemplate.objects.bulk_create((
            ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 1'),
            ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 2'),
            ConsoleServerPortTemplate(device_type=devicetype, name='Console Server Port Template 3'),
        ))

        cls.form_data = {
            'device_type': devicetype.pk,
            'name': 'Console Server Port Template X',
            'type': ConsolePortTypeChoices.TYPE_RJ45,
        }

        cls.bulk_create_data = {
            'device_type': devicetype.pk,
            'name': 'Console Server Port Template [4-6]',
            'type': ConsolePortTypeChoices.TYPE_RJ45,
        }

        cls.bulk_edit_data = {
            'type': ConsolePortTypeChoices.TYPE_RJ45,
        }


class PowerPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
    model = PowerPortTemplate
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')

        PowerPortTemplate.objects.bulk_create((
            PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
            PowerPortTemplate(device_type=devicetype, name='Power Port Template 2'),
            PowerPortTemplate(device_type=devicetype, name='Power Port Template 3'),
        ))

        cls.form_data = {
            'device_type': devicetype.pk,
            'name': 'Power Port Template X',
            'type': PowerPortTypeChoices.TYPE_IEC_C14,
            'maximum_draw': 100,
            'allocated_draw': 50,
        }

        cls.bulk_create_data = {
            'device_type': devicetype.pk,
            'name': 'Power Port Template [4-6]',
            'type': PowerPortTypeChoices.TYPE_IEC_C14,
            'maximum_draw': 100,
            'allocated_draw': 50,
        }

        cls.bulk_edit_data = {
            'type': PowerPortTypeChoices.TYPE_IEC_C14,
            'maximum_draw': 100,
            'allocated_draw': 50,
        }


class PowerOutletTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
    model = PowerOutletTemplate
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')

        PowerOutletTemplate.objects.bulk_create((
            PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 1'),
            PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 2'),
            PowerOutletTemplate(device_type=devicetype, name='Power Outlet Template 3'),
        ))

        powerports = (
            PowerPortTemplate(device_type=devicetype, name='Power Port Template 1'),
        )
        PowerPortTemplate.objects.bulk_create(powerports)

        cls.form_data = {
            'device_type': devicetype.pk,
            'name': 'Power Outlet Template X',
            'type': PowerOutletTypeChoices.TYPE_IEC_C13,
            'power_port': powerports[0].pk,
            'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
        }

        cls.bulk_create_data = {
            'device_type': devicetype.pk,
            'name': 'Power Outlet Template [4-6]',
            'type': PowerOutletTypeChoices.TYPE_IEC_C13,
            'power_port': powerports[0].pk,
            'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
        }

        cls.bulk_edit_data = {
            'type': PowerOutletTypeChoices.TYPE_IEC_C13,
            'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
        }


class InterfaceTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
    model = InterfaceTemplate
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')

        InterfaceTemplate.objects.bulk_create((
            InterfaceTemplate(device_type=devicetype, name='Interface Template 1'),
            InterfaceTemplate(device_type=devicetype, name='Interface Template 2'),
            InterfaceTemplate(device_type=devicetype, name='Interface Template 3'),
        ))

        cls.form_data = {
            'device_type': devicetype.pk,
            'name': 'Interface Template X',
            'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
            'mgmt_only': True,
        }

        cls.bulk_create_data = {
            'device_type': devicetype.pk,
            'name': 'Interface Template [4-6]',
            # Test that a label can be applied to each generated interface templates
            'label': 'Interface Template Label [3-5]',
            'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
            'mgmt_only': True,
        }

        cls.bulk_edit_data = {
            'type': InterfaceTypeChoices.TYPE_1GE_GBIC,
            'mgmt_only': True,
        }


class FrontPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
    model = FrontPortTemplate
    validation_excluded_fields = ('name', 'label', 'rear_port')

    @classmethod
    def setUpTestData(cls):
        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')

        rear_ports = (
            RearPortTemplate(device_type=devicetype, name='Rear Port Template 1'),
            RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'),
            RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'),
            RearPortTemplate(device_type=devicetype, name='Rear Port Template 4'),
            RearPortTemplate(device_type=devicetype, name='Rear Port Template 5'),
            RearPortTemplate(device_type=devicetype, name='Rear Port Template 6'),
        )
        RearPortTemplate.objects.bulk_create(rear_ports)
        front_ports = (
            FrontPortTemplate(device_type=devicetype, name='Front Port Template 1'),
            FrontPortTemplate(device_type=devicetype, name='Front Port Template 2'),
            FrontPortTemplate(device_type=devicetype, name='Front Port Template 3'),
        )
        FrontPortTemplate.objects.bulk_create(front_ports)
        PortTemplateMapping.objects.bulk_create([
            PortTemplateMapping(device_type=devicetype, front_port=front_ports[0], rear_port=rear_ports[0]),
            PortTemplateMapping(device_type=devicetype, front_port=front_ports[1], rear_port=rear_ports[1]),
            PortTemplateMapping(device_type=devicetype, front_port=front_ports[2], rear_port=rear_ports[2]),
        ])

        cls.form_data = {
            'device_type': devicetype.pk,
            'name': 'Front Port X',
            'type': PortTypeChoices.TYPE_8P8C,
            'positions': 1,
            'rear_ports': [f'{rear_ports[3].pk}:1'],
        }

        cls.bulk_create_data = {
            'device_type': devicetype.pk,
            'name': 'Front Port [4-6]',
            'type': PortTypeChoices.TYPE_8P8C,
            'positions': 1,
            'rear_ports': [f'{rp.pk}:1' for rp in rear_ports[3:6]],
        }

        cls.bulk_edit_data = {
            'type': PortTypeChoices.TYPE_8P8C,
        }


class RearPortTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
    model = RearPortTemplate
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')

        RearPortTemplate.objects.bulk_create((
            RearPortTemplate(device_type=devicetype, name='Rear Port Template 1'),
            RearPortTemplate(device_type=devicetype, name='Rear Port Template 2'),
            RearPortTemplate(device_type=devicetype, name='Rear Port Template 3'),
        ))

        cls.form_data = {
            'device_type': devicetype.pk,
            'name': 'Rear Port Template X',
            'type': PortTypeChoices.TYPE_8P8C,
            'positions': 2,
        }

        cls.bulk_create_data = {
            'device_type': devicetype.pk,
            'name': 'Rear Port Template [4-6]',
            'type': PortTypeChoices.TYPE_8P8C,
            'positions': 2,
        }

        cls.bulk_edit_data = {
            'type': PortTypeChoices.TYPE_8P8C,
        }


class ModuleBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
    model = ModuleBayTemplate
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
        devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1')

        ModuleBayTemplate.objects.bulk_create((
            ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 1'),
            ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 2'),
            ModuleBayTemplate(device_type=devicetype, name='Module Bay Template 3'),
        ))

        cls.form_data = {
            'device_type': devicetype.pk,
            'name': 'Module Bay Template X',
        }

        cls.bulk_create_data = {
            'device_type': devicetype.pk,
            'name': 'Module Bay Template [4-6]',
        }

        cls.bulk_edit_data = {
            'description': 'Foo bar',
        }


class DeviceBayTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
    model = DeviceBayTemplate
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
        devicetype = DeviceType.objects.create(
            manufacturer=manufacturer,
            model='Device Type 1',
            slug='device-type-1',
            subdevice_role=SubdeviceRoleChoices.ROLE_PARENT
        )

        DeviceBayTemplate.objects.bulk_create((
            DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 1'),
            DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 2'),
            DeviceBayTemplate(device_type=devicetype, name='Device Bay Template 3'),
        ))

        cls.form_data = {
            'device_type': devicetype.pk,
            'name': 'Device Bay Template X',
        }

        cls.bulk_create_data = {
            'device_type': devicetype.pk,
            'name': 'Device Bay Template [4-6]',
        }

        cls.bulk_edit_data = {
            'description': 'Foo bar',
        }


class InventoryItemTemplateTestCase(ViewTestCases.DeviceComponentTemplateViewTestCase):
    model = InventoryItemTemplate
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        manufacturers = (
            Manufacturer(name='Manufacturer 1', slug='manufacturer-1'),
            Manufacturer(name='Manufacturer 2', slug='manufacturer-2'),
        )
        Manufacturer.objects.bulk_create(manufacturers)
        devicetype = DeviceType.objects.create(
            manufacturer=manufacturers[0], model='Device Type 1', slug='device-type-1'
        )

        inventory_item_templates = (
            InventoryItemTemplate(
                device_type=devicetype, name='Inventory Item Template 1', manufacturer=manufacturers[0]
            ),
            InventoryItemTemplate(
                device_type=devicetype, name='Inventory Item Template 2', manufacturer=manufacturers[0]
            ),
            InventoryItemTemplate(
                device_type=devicetype, name='Inventory Item Template 3', manufacturer=manufacturers[0]
            ),
        )
        for item in inventory_item_templates:
            item.save()

        cls.form_data = {
            'device_type': devicetype.pk,
            'name': 'Inventory Item Template X',
            'manufacturer': manufacturers[1].pk,
        }

        cls.bulk_create_data = {
            'device_type': devicetype.pk,
            'name': 'Inventory Item Template [4-6]',
            'manufacturer': manufacturers[1].pk,
        }

        cls.bulk_edit_data = {
            'description': 'Foo bar',
        }


class DeviceRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
    model = DeviceRole

    @classmethod
    def setUpTestData(cls):

        roles = [
            DeviceRole(name='Device Role 1', slug='device-role-1'),
            DeviceRole(name='Device Role 2', slug='device-role-2'),
            DeviceRole(name='Device Role 3', slug='device-role-3'),
            DeviceRole(name='Device Role 4', slug='device-role-4'),
        ]
        for role in roles:
            role.save()

        roles.append(DeviceRole.objects.create(name='Device Role 5', slug='device-role-5', parent=roles[3]))
        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Device Role X',
            'slug': 'device-role-x',
            'color': 'c0c0c0',
            'vm_role': False,
            'description': 'New device role',
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "name,slug,color",
            "Device Role 6,device-role-6,ff0000",
            "Device Role 7,device-role-7,00ff00",
            "Device Role 8,device-role-8,0000ff",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{roles[0].pk},Device Role 7,New description7",
            f"{roles[1].pk},Device Role 8,New description8",
            f"{roles[2].pk},Device Role 9,New description9",
            f"{roles[4].pk},Device Role 10,New description10",
        )

        cls.bulk_edit_data = {
            'color': '00ff00',
            'description': 'New description',
        }


class PlatformTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
    model = Platform

    @classmethod
    def setUpTestData(cls):

        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')

        platforms = (
            Platform(name='Platform 1', slug='platform-1', manufacturer=manufacturer),
            Platform(name='Platform 2', slug='platform-2', manufacturer=manufacturer),
            Platform(name='Platform 3', slug='platform-3', manufacturer=manufacturer),
        )
        for platform in platforms:
            platform.save()

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Platform X',
            'slug': 'platform-x',
            'manufacturer': manufacturer.pk,
            'description': 'A new platform',
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "name,slug,description",
            "Platform 4,platform-4,Fourth platform",
            "Platform 5,platform-5,Fifth platform",
            "Platform 6,platform-6,Sixth platform",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{platforms[0].pk},Foo,New description",
            f"{platforms[1].pk},Bar,New description",
            f"{platforms[2].pk},Baz,New description",
        )

        cls.bulk_edit_data = {
            'description': 'New description',
        }


class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
    model = Device

    @classmethod
    def setUpTestData(cls):

        sites = (
            Site(name='Site 1', slug='site-1'),
            Site(name='Site 2', slug='site-2'),
        )
        Site.objects.bulk_create(sites)

        location = Location(site=sites[0], name='Location 1', slug='location-1')
        location.save()

        racks = (
            Rack(name='Rack 1', site=sites[0], location=location),
            Rack(name='Rack 2', site=sites[1]),
        )
        Rack.objects.bulk_create(racks)

        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')

        devicetypes = (
            DeviceType(model='Device Type 1', slug='device-type-1', manufacturer=manufacturer),
            DeviceType(model='Device Type 2', slug='device-type-2', manufacturer=manufacturer),
        )
        DeviceType.objects.bulk_create(devicetypes)

        roles = (
            DeviceRole(name='Device Role 1', slug='device-role-1'),
            DeviceRole(name='Device Role 2', slug='device-role-2'),
        )
        for role in roles:
            role.save()

        platforms = (
            Platform(name='Platform 1', slug='platform-1'),
            Platform(name='Platform 2', slug='platform-2'),
        )
        for platform in platforms:
            platform.save()

        devices = (
            Device(
                name='Device 1',
                site=sites[0],
                rack=racks[0],
                device_type=devicetypes[0],
                role=roles[0],
                platform=platforms[0],
            ),
            Device(
                name='Device 2',
                site=sites[0],
                rack=racks[0],
                device_type=devicetypes[0],
                role=roles[0],
                platform=platforms[0],
            ),
            Device(
                name='Device 3',
                site=sites[0],
                rack=racks[0],
                device_type=devicetypes[0],
                role=roles[0],
                platform=platforms[0],
            ),
        )
        Device.objects.bulk_create(devices)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        VirtualChassis.objects.create(name='Virtual Chassis 1')

        cls.form_data = {
            'device_type': devicetypes[1].pk,
            'role': roles[1].pk,
            'tenant': None,
            'platform': platforms[1].pk,
            'name': 'Device X',
            'serial': '123456',
            'asset_tag': 'ABCDEF',
            'site': sites[1].pk,
            'rack': racks[1].pk,
            'position': 1,
            'face': DeviceFaceChoices.FACE_FRONT,
            'latitude': Decimal('35.780000'),
            'longitude': Decimal('-78.642000'),
            'status': DeviceStatusChoices.STATUS_PLANNED,
            'primary_ip4': None,
            'primary_ip6': None,
            'cluster': None,
            'virtual_chassis': None,
            'vc_position': None,
            'vc_priority': None,
            'comments': 'A new device',
            'tags': [t.pk for t in tags],
            'local_context_data': None,
        }

        cls.csv_data = (
            (
                "role,manufacturer,device_type,status,name,site,location,rack,position,face,virtual_chassis,"
                "vc_position,vc_priority"
            ),
            (
                "Device Role 1,Manufacturer 1,Device Type 1,active,Device 4,Site 1,Location 1,Rack 1,10,front,"
                "Virtual Chassis 1,1,10"
            ),
            (
                "Device Role 1,Manufacturer 1,Device Type 1,active,Device 5,Site 1,Location 1,Rack 1,20,front,"
                "Virtual Chassis 1,2,20"
            ),
            (
                "Device Role 1,Manufacturer 1,Device Type 1,active,Device 6,Site 1,Location 1,Rack 1,30,front,"
                "Virtual Chassis 1,3,30"
            ),
        )

        cls.csv_update_data = (
            "id,status",
            f"{devices[0].pk},{DeviceStatusChoices.STATUS_DECOMMISSIONING}",
            f"{devices[1].pk},{DeviceStatusChoices.STATUS_DECOMMISSIONING}",
            f"{devices[2].pk},{DeviceStatusChoices.STATUS_DECOMMISSIONING}",
        )

        cls.bulk_edit_data = {
            'device_type': devicetypes[1].pk,
            'role': roles[1].pk,
            'tenant': None,
            'platform': platforms[1].pk,
            'serial': '123456',
            'status': DeviceStatusChoices.STATUS_DECOMMISSIONING,
        }

    def test_device_consoleports(self):
        self.add_permissions('dcim.view_device', 'dcim.view_consoleport')
        device = Device.objects.first()
        console_ports = (
            ConsolePort(device=device, name='Console Port 1'),
            ConsolePort(device=device, name='Console Port 2'),
            ConsolePort(device=device, name='Console Port 3'),
        )
        ConsolePort.objects.bulk_create(console_ports)

        url = reverse('dcim:device_consoleports', kwargs={'pk': device.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_device_consoleserverports(self):
        self.add_permissions('dcim.view_device', 'dcim.view_consoleserverport')
        device = Device.objects.first()
        console_server_ports = (
            ConsoleServerPort(device=device, name='Console Server Port 1'),
            ConsoleServerPort(device=device, name='Console Server Port 2'),
            ConsoleServerPort(device=device, name='Console Server Port 3'),
        )
        ConsoleServerPort.objects.bulk_create(console_server_ports)

        url = reverse('dcim:device_consoleserverports', kwargs={'pk': device.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_device_powerports(self):
        self.add_permissions('dcim.view_device', 'dcim.view_powerport')
        device = Device.objects.first()
        power_ports = (
            PowerPort(device=device, name='Power Port 1'),
            PowerPort(device=device, name='Power Port 2'),
            PowerPort(device=device, name='Power Port 3'),
        )
        PowerPort.objects.bulk_create(power_ports)

        url = reverse('dcim:device_powerports', kwargs={'pk': device.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_device_poweroutlets(self):
        self.add_permissions('dcim.view_device', 'dcim.view_poweroutlet')
        device = Device.objects.first()
        power_outlets = (
            PowerOutlet(device=device, name='Power Outlet 1'),
            PowerOutlet(device=device, name='Power Outlet 2'),
            PowerOutlet(device=device, name='Power Outlet 3'),
        )
        PowerOutlet.objects.bulk_create(power_outlets)

        url = reverse('dcim:device_poweroutlets', kwargs={'pk': device.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_device_interfaces(self):
        self.add_permissions('dcim.view_device', 'dcim.view_interface')
        device = Device.objects.first()
        interfaces = (
            Interface(device=device, name='Interface 1'),
            Interface(device=device, name='Interface 2'),
            Interface(device=device, name='Interface 3'),
        )
        Interface.objects.bulk_create(interfaces)

        url = reverse('dcim:device_interfaces', kwargs={'pk': device.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_device_rearports(self):
        self.add_permissions('dcim.view_device', 'dcim.view_rearport')
        device = Device.objects.first()
        rear_ports = (
            RearPort(device=device, name='Rear Port 1'),
            RearPort(device=device, name='Rear Port 2'),
            RearPort(device=device, name='Rear Port 3'),
        )
        RearPort.objects.bulk_create(rear_ports)

        url = reverse('dcim:device_rearports', kwargs={'pk': device.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_device_frontports(self):
        self.add_permissions('dcim.view_device', 'dcim.view_frontport', 'dcim.view_rearport')
        device = Device.objects.first()
        rear_ports = (
            RearPort(device=device, name='Rear Port 1'),
            RearPort(device=device, name='Rear Port 2'),
            RearPort(device=device, name='Rear Port 3'),
        )
        RearPort.objects.bulk_create(rear_ports)
        front_ports = (
            FrontPort(device=device, name='Front Port Template 1'),
            FrontPort(device=device, name='Front Port Template 2'),
            FrontPort(device=device, name='Front Port Template 3'),
        )
        FrontPort.objects.bulk_create(front_ports)
        PortMapping.objects.bulk_create([
            PortMapping(device=device, front_port=front_ports[0], rear_port=rear_ports[0]),
            PortMapping(device=device, front_port=front_ports[1], rear_port=rear_ports[1]),
            PortMapping(device=device, front_port=front_ports[2], rear_port=rear_ports[2]),
        ])

        url = reverse('dcim:device_frontports', kwargs={'pk': device.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_device_modulebays(self):
        self.add_permissions('dcim.view_device', 'dcim.view_modulebay')
        device = Device.objects.first()
        ModuleBay.objects.create(device=device, name='Module Bay 1')
        ModuleBay.objects.create(device=device, name='Module Bay 2')
        ModuleBay.objects.create(device=device, name='Module Bay 3')

        url = reverse('dcim:device_modulebays', kwargs={'pk': device.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_device_devicebays(self):
        self.add_permissions('dcim.view_device', 'dcim.view_devicebay')
        device = Device.objects.first()
        device_bays = (
            DeviceBay(device=device, name='Device Bay 1'),
            DeviceBay(device=device, name='Device Bay 2'),
            DeviceBay(device=device, name='Device Bay 3'),
        )
        DeviceBay.objects.bulk_create(device_bays)

        url = reverse('dcim:device_devicebays', kwargs={'pk': device.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_device_inventory(self):
        self.add_permissions('dcim.view_device', 'dcim.view_inventoryitem')
        device = Device.objects.first()
        inventory_items = (
            InventoryItem(device=device, name='Inventory Item 1'),
            InventoryItem(device=device, name='Inventory Item 2'),
            InventoryItem(device=device, name='Inventory Item 3'),
        )
        for item in inventory_items:
            item.save()

        url = reverse('dcim:device_inventory', kwargs={'pk': device.pk})
        self.assertHttpStatus(self.client.get(url), 200)

    def test_device_renderconfig(self):
        configtemplate = ConfigTemplate.objects.create(
            name='Test Config Template',
            template_code='Config for device {{ device.name }}'
        )
        device = Device.objects.first()
        device.config_template = configtemplate
        device.save()
        url = reverse('dcim:device_render-config', kwargs={'pk': device.pk})

        # User with only view permission should NOT be able to render config
        self.add_permissions('dcim.view_device')
        self.assertHttpStatus(self.client.get(url), 403)

        # With render_config permission added should be able to render config
        self.add_permissions('dcim.render_config_device')
        self.assertHttpStatus(self.client.get(url), 200)

        # With view permission removed should NOT be able to render config
        self.remove_permissions('dcim.view_device')
        self.assertHttpStatus(self.client.get(url), 403)

    def test_device_renderconfig_with_config_template_id(self):
        default_template = ConfigTemplate.objects.create(
            name='Default Template',
            template_code='Default config for {{ device.name }}'
        )
        override_template = ConfigTemplate.objects.create(
            name='Override Template',
            template_code='Override config for {{ device.name }}'
        )
        device = Device.objects.first()
        device.config_template = default_template
        device.save()

        self.add_permissions('dcim.view_device', 'dcim.render_config_device', 'extras.view_configtemplate')
        url = reverse('dcim:device_render-config', kwargs={'pk': device.pk})

        # Render with override config_template_id
        response = self.client.get(url, {'config_template_id': override_template.pk})
        self.assertHttpStatus(response, 200)
        self.assertIn(b'Override config for', response.content)

        # Render with nonexistent config_template_id still returns 200 with error message
        response = self.client.get(url, {'config_template_id': 999999})
        self.assertHttpStatus(response, 200)
        self.assertIn(b'Error rendering template', response.content)

        # Render with non-integer config_template_id still returns 200 with error message
        response = self.client.get(url, {'config_template_id': 'abc'})
        self.assertHttpStatus(response, 200)
        self.assertIn(b'Error rendering template', response.content)

        # Without view_configtemplate permission, override template should not be accessible
        self.remove_permissions('extras.view_configtemplate')
        response = self.client.get(url, {'config_template_id': override_template.pk})
        self.assertHttpStatus(response, 200)
        self.assertIn(b'Error rendering template', response.content)

    def test_device_role_display_colored(self):
        parent_role = DeviceRole.objects.create(name='Parent Role', slug='parent-role', color='111111')
        child_role = DeviceRole.objects.create(name='Child Role', slug='child-role', parent=parent_role, color='aa00bb')

        device = Device.objects.first()
        device.role = child_role
        device.save()

        self.add_permissions('dcim.view_device')
        response = self.client.get(device.get_absolute_url())

        self.assertHttpStatus(response, 200)
        self.assertContains(response, 'Parent Role')
        self.assertContains(response, 'Child Role')
        self.assertContains(response, 'background-color: #aa00bb')
        self.assertNotContains(response, 'background-color: #111111')

    def test_bulk_import_duplicate_ids_error_message(self):
        device = Device.objects.first()
        csv_data = (
            "id,role",
            f"{device.pk},Device Role 1",
            f"{device.pk},Device Role 2",
        )

        self.add_permissions(
            'dcim.view_device',
            'dcim.add_device',
            'dcim.change_device',
            'dcim.view_devicerole',
        )
        response = self.client.post(
            self._get_url('bulk_import'),
            {
                'data': '\n'.join(csv_data),
                'format': ImportFormatChoices.CSV,
                'csv_delimiter': CSVDelimiterChoices.AUTO,
            },
            follow=True
        )

        self.assertEqual(response.status_code, 200)
        self.assertIn(
            f'Duplicate objects found: Device with ID(s) {device.pk} appears multiple times',
            response.content.decode('utf-8')
        )


class ModuleTestCase(
    # Module does not support bulk renaming (no name field) or
    # bulk creation (need to specify module bays)
    ViewTestCases.GetObjectViewTestCase,
    ViewTestCases.GetObjectChangelogViewTestCase,
    ViewTestCases.CreateObjectViewTestCase,
    ViewTestCases.EditObjectViewTestCase,
    ViewTestCases.DeleteObjectViewTestCase,
    ViewTestCases.ListObjectsViewTestCase,
    ViewTestCases.BulkImportObjectsViewTestCase,
    ViewTestCases.BulkEditObjectsViewTestCase,
    ViewTestCases.BulkDeleteObjectsViewTestCase,
):
    model = Module

    @classmethod
    def setUpTestData(cls):
        manufacturer = Manufacturer.objects.create(name='Generic', slug='generic')
        module_type_profile = ModuleTypeProfile.objects.create(
            name='Module Type Profile 1',
            schema={
                'properties': {
                    'media': {
                        'title': 'Media',
                        'type': 'array',
                        'items': {
                            'type': 'string',
                            'enum': ['copper', 'sfp', 'qsfp28'],
                        },
                    },
                },
            },
        )
        devices = (
            create_test_device('Device 1'),
            create_test_device('Device 2'),
        )

        module_types = (
            ModuleType(
                manufacturer=manufacturer,
                model='Module Type 1',
                profile=module_type_profile,
                attribute_data={'media': ['copper', 'qsfp28']},
            ),
            ModuleType(manufacturer=manufacturer, model='Module Type 2'),
            ModuleType(manufacturer=manufacturer, model='Module Type 3'),
            ModuleType(manufacturer=manufacturer, model='Module Type 4'),
        )
        ModuleType.objects.bulk_create(module_types)

        module_bays = (
            ModuleBay(device=devices[0], name='Module Bay 1'),
            ModuleBay(device=devices[0], name='Module Bay 2'),
            ModuleBay(device=devices[0], name='Module Bay 3'),
            ModuleBay(device=devices[0], name='Module Bay 4'),
            ModuleBay(device=devices[0], name='Module Bay 5'),
            ModuleBay(device=devices[1], name='Module Bay 1'),
            ModuleBay(device=devices[1], name='Module Bay 2'),
            ModuleBay(device=devices[1], name='Module Bay 3'),
            ModuleBay(device=devices[1], name='Module Bay 4'),
            ModuleBay(device=devices[1], name='Module Bay 5'),
        )
        for module_bay in module_bays:
            module_bay.save()

        modules = (
            Module(device=devices[0], module_bay=module_bays[0], module_type=module_types[0]),
            Module(device=devices[0], module_bay=module_bays[1], module_type=module_types[1]),
            Module(device=devices[0], module_bay=module_bays[2], module_type=module_types[2]),
        )
        Module.objects.bulk_create(modules)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'device': devices[0].pk,
            'module_bay': module_bays[3].pk,
            'module_type': module_types[0].pk,
            'status': ModuleStatusChoices.STATUS_ACTIVE,
            'serial': 'A',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_edit_data = {
            'module_type': module_types[3].pk,
            'status': ModuleStatusChoices.STATUS_PLANNED,
        }

        cls.csv_data = (
            "device,module_bay,module_type,status,serial,asset_tag",
            "Device 2,Module Bay 1,Module Type 1,active,A,A",
            "Device 2,Module Bay 2,Module Type 2,planned,B,B",
            "Device 2,Module Bay 3,Module Type 3,failed,C,C",
        )

        cls.csv_update_data = (
            "id,status,serial",
            f"{modules[0].pk},offline,Serial 2",
            f"{modules[1].pk},offline,Serial 3",
            f"{modules[2].pk},offline,Serial 1",
        )

    def test_module_detail_includes_module_type_profile(self):
        self.add_permissions(
            'dcim.view_module',
            'dcim.view_moduletype',
            'dcim.view_moduletypeprofile',
        )
        response = self.client.get(self._get_queryset().first().get_absolute_url())

        self.assertContains(response, 'Module Type Profile 1')

    @tag('regression')
    def test_module_detail_renders_module_type_attribute_lists(self):
        self.add_permissions(
            'dcim.view_module',
            'dcim.view_moduletype',
            'dcim.view_moduletypeprofile',
        )
        response = self.client.get(self._get_queryset().first().get_absolute_url())

        self.assertHttpStatus(response, 200)
        self.assertContains(response, 'Media')
        self.assertContains(response, 'copper, qsfp28')

    def test_module_component_replication(self):
        self.add_permissions(
            'dcim.view_module',
            'dcim.add_module',
            'dcim.view_moduletype',
            'dcim.view_device',
            'dcim.view_modulebay',
            'dcim.view_interface',
            'extras.view_tag',
        )

        # Add 5 InterfaceTemplates to a ModuleType
        module_type = ModuleType.objects.first()
        interface_templates = [
            InterfaceTemplate(module_type=module_type, name=f'Interface {i}') for i in range(1, 6)
        ]
        InterfaceTemplate.objects.bulk_create(interface_templates)

        form_data = self.form_data.copy()
        device = Device.objects.get(pk=form_data['device'])

        # Create a module *without* replicating components
        form_data['replicate_components'] = False
        request = {
            'path': self._get_url('add'),
            'data': post_data(form_data),
        }
        self.assertHttpStatus(self.client.post(**request), 302)
        self.assertEqual(Interface.objects.filter(device=device).count(), 0)

        # Create a second module (in the next bay) with replicated components
        form_data['module_bay'] += 1
        form_data['replicate_components'] = True
        request = {
            'path': self._get_url('add'),
            'data': post_data(form_data),
        }
        self.assertHttpStatus(self.client.post(**request), 302)
        self.assertEqual(Interface.objects.filter(device=device).count(), 5)

    def test_module_bulk_replication(self):
        self.add_permissions(
            'dcim.view_module',
            'dcim.add_module',
            'dcim.view_moduletype',
            'dcim.view_device',
            'dcim.view_modulebay',
            'dcim.view_interface',
        )

        # Add 5 InterfaceTemplates to a ModuleType
        module_type = ModuleType.objects.first()
        interface_templates = [
            InterfaceTemplate(module_type=module_type, name=f'Interface {i}')
            for i in range(1, 6)
        ]
        InterfaceTemplate.objects.bulk_create(interface_templates)

        # Create a module *without* replicating components
        device = Device.objects.get(name='Device 2')
        module_bay = ModuleBay.objects.get(device=device, name='Module Bay 4')
        csv_data = [
            "device,module_bay,module_type,status,replicate_components",
            f"{device.name},{module_bay.name},{module_type.model},active,false"
        ]
        request = {
            'path': self._get_url('bulk_import'),
            'data': {
                'data': '\n'.join(csv_data),
                'format': ImportFormatChoices.CSV,
                'csv_delimiter': CSVDelimiterChoices.AUTO,
            }
        }

        initial_count = Module.objects.count()
        self.assertHttpStatus(self.client.post(**request), 302)
        self.assertEqual(Module.objects.count(), initial_count + len(csv_data) - 1)
        self.assertEqual(Interface.objects.filter(device=device).count(), 0)

        # Create a second module (in the next bay) with replicated components
        module_bay = ModuleBay.objects.get(device=device, name='Module Bay 5')
        csv_data[1] = f"{device.name},{module_bay.name},{module_type.model},active,true"
        request = {
            'path': self._get_url('bulk_import'),
            'data': {
                'data': '\n'.join(csv_data),
                'format': ImportFormatChoices.CSV,
                'csv_delimiter': CSVDelimiterChoices.AUTO,
            }
        }

        initial_count = Module.objects.count()
        self.assertHttpStatus(self.client.post(**request), 302)
        self.assertEqual(Module.objects.count(), initial_count + len(csv_data) - 1)
        self.assertEqual(Interface.objects.filter(device=device).count(), 5)

    def test_module_component_adoption(self):
        self.add_permissions(
            'dcim.view_module',
            'dcim.add_module',
            'dcim.view_moduletype',
            'dcim.view_device',
            'dcim.view_modulebay',
            'dcim.view_interface',
            'dcim.change_interface',
            'extras.view_tag',
        )

        interface_name = "Interface-1"

        # Add an interface to the ModuleType
        module_type = ModuleType.objects.first()
        InterfaceTemplate(module_type=module_type, name=interface_name).save()

        form_data = self.form_data.copy()
        device = Device.objects.get(pk=form_data['device'])

        # Create an interface to be adopted
        interface = Interface(device=device, name=interface_name, type=InterfaceTypeChoices.TYPE_10GE_FIXED)
        interface.save()

        # Ensure that interface is created with no module
        self.assertIsNone(interface.module)

        # Create a module with adopted components
        form_data['module_type'] = module_type
        form_data['replicate_components'] = False
        form_data['adopt_components'] = True
        request = {
            'path': self._get_url('add'),
            'data': post_data(form_data),
        }

        self.assertHttpStatus(self.client.post(**request), 302)

        # Re-retrieve interface to get new module id
        interface.refresh_from_db()

        # Check that the Interface now has a module
        self.assertIsNotNone(interface.module)

    def test_module_bulk_adoption(self):
        self.add_permissions(
            'dcim.view_module',
            'dcim.add_module',
            'dcim.view_moduletype',
            'dcim.view_device',
            'dcim.view_modulebay',
            'dcim.view_interface',
            'dcim.change_interface',
        )

        interface_name = "Interface-1"

        # Add an interface to the ModuleType
        module_type = ModuleType.objects.first()
        InterfaceTemplate(module_type=module_type, name=interface_name).save()

        form_data = self.form_data.copy()
        device = Device.objects.get(pk=form_data['device'])

        # Create an interface to be adopted
        interface = Interface(device=device, name=interface_name, type=InterfaceTypeChoices.TYPE_10GE_FIXED)
        interface.save()

        # Ensure that interface is created with no module
        self.assertIsNone(interface.module)

        # Create a module with adopted components
        module_bay = ModuleBay.objects.get(device=device, name='Module Bay 4')
        csv_data = [
            "device,module_bay,module_type,status,replicate_components,adopt_components",
            f"{device.name},{module_bay.name},{module_type.model},active,false,true"
        ]
        request = {
            'path': self._get_url('bulk_import'),
            'data': {
                'data': '\n'.join(csv_data),
                'format': ImportFormatChoices.CSV,
                'csv_delimiter': CSVDelimiterChoices.AUTO,
            }
        }

        initial_count = self._get_queryset().count()
        self.assertHttpStatus(self.client.post(**request), 302)
        self.assertEqual(self._get_queryset().count(), initial_count + len(csv_data) - 1)

        # Re-retrieve interface to get new module id
        interface.refresh_from_db()

        # Check that the Interface now has a module
        self.assertIsNotNone(interface.module)


class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
    model = ConsolePort
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        device = create_test_device('Device 1')

        console_ports = (
            ConsolePort(device=device, name='Console Port 1'),
            ConsolePort(device=device, name='Console Port 2'),
            ConsolePort(device=device, name='Console Port 3'),
        )
        ConsolePort.objects.bulk_create(console_ports)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'device': device.pk,
            'name': 'Console Port X',
            'type': ConsolePortTypeChoices.TYPE_RJ45,
            'description': 'A console port',
            'tags': sorted([t.pk for t in tags]),
        }

        cls.bulk_create_data = {
            'device': device.pk,
            'name': 'Console Port [4-6]',
            # Test that a label can be applied to each generated console ports
            'label': 'Serial[3-5]',
            'type': ConsolePortTypeChoices.TYPE_RJ45,
            'description': 'A console port',
            'tags': sorted([t.pk for t in tags]),
        }

        cls.bulk_edit_data = {
            'type': ConsolePortTypeChoices.TYPE_RJ45,
            'description': 'New description',
        }

        cls.csv_data = (
            "device,name",
            "Device 1,Console Port 4",
            "Device 1,Console Port 5",
            "Device 1,Console Port 6",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{console_ports[0].pk},Console Port 7,New description7",
            f"{console_ports[1].pk},Console Port 8,New description8",
            f"{console_ports[2].pk},Console Port 9,New description9",
        )

    def test_bulk_add_components_with_changelog_message(self):
        self.add_permissions('dcim.view_consoleport', 'dcim.view_device')
        device1 = Device.objects.get(name='Device 1')
        device2 = create_test_device('Device 2')
        changelog_message = 'Bulk-created console ports'

        obj_perm = ObjectPermission(
            name='Test permission',
            actions=['add'],
        )
        obj_perm.save()
        obj_perm.users.add(self.user)
        obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))

        request = {
            'path': reverse('dcim:device_bulk_add_consoleport'),
            'data': post_data({
                'pk': [device1.pk, device2.pk],
                'name': 'Console Port Bulk',
                'type': ConsolePortTypeChoices.TYPE_RJ45,
                'description': 'Bulk-created console port',
                'changelog_message': changelog_message,
                '_create': True,
            }),
        }

        initial_count = self._get_queryset().count()
        response = self.client.post(**request)
        self.assertHttpStatus(response, 302)
        self.assertEqual(initial_count + 2, self._get_queryset().count())

        created_ports = list(ConsolePort.objects.filter(name='Console Port Bulk').order_by('device_id'))
        self.assertEqual(len(created_ports), 2)
        self.assertEqual([port.device_id for port in created_ports], [device1.pk, device2.pk])

        objectchanges = ObjectChange.objects.filter(
            action=ObjectChangeActionChoices.ACTION_CREATE,
            changed_object_type=ContentType.objects.get_for_model(ConsolePort),
            changed_object_id__in=[port.pk for port in created_ports],
        )
        self.assertEqual(objectchanges.count(), 2)
        for objectchange in objectchanges:
            self.assertEqual(objectchange.message, changelog_message)

    def test_trace(self):
        self.add_permissions(
            'dcim.view_consoleport',
            'dcim.view_consoleserverport',
            'dcim.view_cable',
            'dcim.view_device',
        )
        consoleport = ConsolePort.objects.first()
        consoleserverport = ConsoleServerPort.objects.create(
            device=consoleport.device,
            name='Console Server Port 1'
        )
        Cable(a_terminations=[consoleport], b_terminations=[consoleserverport]).save()

        response = self.client.get(reverse('dcim:consoleport_trace', kwargs={'pk': consoleport.pk}))
        self.assertHttpStatus(response, 200)


class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
    model = ConsoleServerPort
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        device = create_test_device('Device 1')

        console_server_ports = (
            ConsoleServerPort(device=device, name='Console Server Port 1'),
            ConsoleServerPort(device=device, name='Console Server Port 2'),
            ConsoleServerPort(device=device, name='Console Server Port 3'),
        )
        ConsoleServerPort.objects.bulk_create(console_server_ports)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'device': device.pk,
            'name': 'Console Server Port X',
            'type': ConsolePortTypeChoices.TYPE_RJ45,
            'description': 'A console server port',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_create_data = {
            'device': device.pk,
            'name': 'Console Server Port [4-6]',
            'type': ConsolePortTypeChoices.TYPE_RJ45,
            'description': 'A console server port',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_edit_data = {
            'type': ConsolePortTypeChoices.TYPE_RJ11,
            'description': 'New description',
        }

        cls.csv_data = (
            "device,name",
            "Device 1,Console Server Port 4",
            "Device 1,Console Server Port 5",
            "Device 1,Console Server Port 6",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{console_server_ports[0].pk},Console Server Port 7,New description 7",
            f"{console_server_ports[1].pk},Console Server Port 8,New description 8",
            f"{console_server_ports[2].pk},Console Server Port 9,New description 9",
        )

    def test_trace(self):
        self.add_permissions(
            'dcim.view_consoleserverport',
            'dcim.view_consoleport',
            'dcim.view_cable',
            'dcim.view_device',
        )
        consoleserverport = ConsoleServerPort.objects.first()
        consoleport = ConsolePort.objects.create(
            device=consoleserverport.device,
            name='Console Port 1'
        )
        Cable(a_terminations=[consoleserverport], b_terminations=[consoleport]).save()

        response = self.client.get(reverse('dcim:consoleserverport_trace', kwargs={'pk': consoleserverport.pk}))
        self.assertHttpStatus(response, 200)


class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
    model = PowerPort
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        device = create_test_device('Device 1')

        power_ports = (
            PowerPort(device=device, name='Power Port 1'),
            PowerPort(device=device, name='Power Port 2'),
            PowerPort(device=device, name='Power Port 3'),
        )
        PowerPort.objects.bulk_create(power_ports)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'device': device.pk,
            'name': 'Power Port X',
            'type': PowerPortTypeChoices.TYPE_IEC_C14,
            'maximum_draw': 100,
            'allocated_draw': 50,
            'description': 'A power port',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_create_data = {
            'device': device.pk,
            'name': 'Power Port [4-6]]',
            'type': PowerPortTypeChoices.TYPE_IEC_C14,
            'maximum_draw': 100,
            'allocated_draw': 50,
            'description': 'A power port',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_edit_data = {
            'type': PowerPortTypeChoices.TYPE_IEC_C14,
            'maximum_draw': 100,
            'allocated_draw': 50,
            'description': 'New description',
        }

        cls.csv_data = (
            "device,name",
            "Device 1,Power Port 4",
            "Device 1,Power Port 5",
            "Device 1,Power Port 6",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{power_ports[0].pk},Power Port 7,New description7",
            f"{power_ports[1].pk},Power Port 8,New description8",
            f"{power_ports[2].pk},Power Port 9,New description9",
        )

    def test_trace(self):
        self.add_permissions(
            'dcim.view_powerport',
            'dcim.view_poweroutlet',
            'dcim.view_cable',
            'dcim.view_device',
        )
        powerport = PowerPort.objects.first()
        poweroutlet = PowerOutlet.objects.create(
            device=powerport.device,
            name='Power Outlet 1'
        )
        Cable(a_terminations=[powerport], b_terminations=[poweroutlet]).save()

        response = self.client.get(reverse('dcim:powerport_trace', kwargs={'pk': powerport.pk}))
        self.assertHttpStatus(response, 200)


class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
    model = PowerOutlet
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        device = create_test_device('Device 1')

        powerports = (
            PowerPort(device=device, name='Power Port 1'),
            PowerPort(device=device, name='Power Port 2'),
        )
        PowerPort.objects.bulk_create(powerports)

        power_outlets = (
            PowerOutlet(device=device, name='Power Outlet 1', power_port=powerports[0]),
            PowerOutlet(device=device, name='Power Outlet 2', power_port=powerports[0]),
            PowerOutlet(device=device, name='Power Outlet 3', power_port=powerports[0]),
        )
        PowerOutlet.objects.bulk_create(power_outlets)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'device': device.pk,
            'name': 'Power Outlet X',
            'type': PowerOutletTypeChoices.TYPE_IEC_C13,
            'status': PowerOutletStatusChoices.STATUS_ENABLED,
            'power_port': powerports[1].pk,
            'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
            'description': 'A power outlet',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_create_data = {
            'device': device.pk,
            'name': 'Power Outlet [4-6]',
            'type': PowerOutletTypeChoices.TYPE_IEC_C13,
            'status': PowerOutletStatusChoices.STATUS_ENABLED,
            'power_port': powerports[1].pk,
            'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
            'description': 'A power outlet',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_edit_data = {
            'type': PowerOutletTypeChoices.TYPE_IEC_C15,
            'status': PowerOutletStatusChoices.STATUS_ENABLED,
            'power_port': powerports[1].pk,
            'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_B,
            'description': 'New description',
        }

        cls.csv_data = (
            "device,name",
            "Device 1,Power Outlet 4",
            "Device 1,Power Outlet 5",
            "Device 1,Power Outlet 6",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{power_outlets[0].pk},Power Outlet 7,New description7",
            f"{power_outlets[1].pk},Power Outlet 8,New description8",
            f"{power_outlets[2].pk},Power Outlet 9,New description9",
        )

    def test_trace(self):
        self.add_permissions(
            'dcim.view_poweroutlet',
            'dcim.view_powerport',
            'dcim.view_cable',
            'dcim.view_device',
        )
        poweroutlet = PowerOutlet.objects.first()
        powerport = PowerPort.objects.first()
        Cable(a_terminations=[poweroutlet], b_terminations=[powerport]).save()

        response = self.client.get(reverse('dcim:poweroutlet_trace', kwargs={'pk': poweroutlet.pk}))
        self.assertHttpStatus(response, 200)


class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
    model = Interface
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        device = create_test_device('Device 1')

        interfaces = (
            Interface(device=device, name='Interface 1'),
            Interface(device=device, name='Interface 2'),
            Interface(device=device, name='Interface 3'),
            Interface(device=device, name='LAG', type=InterfaceTypeChoices.TYPE_LAG),
            Interface(device=device, name='_BRIDGE', type=InterfaceTypeChoices.TYPE_VIRTUAL),  # Must be ordered last
        )
        Interface.objects.bulk_create(interfaces)

        vlans = (
            VLAN(vid=1, name='VLAN1', site=device.site),
            VLAN(vid=101, name='VLAN101', site=device.site),
            VLAN(vid=102, name='VLAN102', site=device.site),
            VLAN(vid=103, name='VLAN103', site=device.site),
        )
        VLAN.objects.bulk_create(vlans)

        wireless_lans = (
            WirelessLAN(ssid='WLAN1'),
            WirelessLAN(ssid='WLAN2'),
        )
        WirelessLAN.objects.bulk_create(wireless_lans)

        vrfs = (
            VRF(name='VRF 1'),
            VRF(name='VRF 2'),
            VRF(name='VRF 3'),
        )
        VRF.objects.bulk_create(vrfs)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'device': device.pk,
            'name': 'Interface X',
            'type': InterfaceTypeChoices.TYPE_OTHER,
            'enabled': False,
            'bridge': interfaces[4].pk,
            'lag': interfaces[3].pk,
            'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
            'mtu': 65000,
            'speed': 16_000_000_000,
            'duplex': 'full',
            'mgmt_only': True,
            'description': 'A front port',
            'mode': InterfaceModeChoices.MODE_TAGGED,
            'tx_power': 10,
            'poe_mode': InterfacePoEModeChoices.MODE_PSE,
            'poe_type': InterfacePoETypeChoices.TYPE_1_8023AF,
            'untagged_vlan': vlans[0].pk,
            'tagged_vlans': [v.pk for v in vlans[1:4]],
            'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
            'vrf': vrfs[0].pk,
            'tags': [t.pk for t in tags],
        }

        cls.bulk_create_data = {
            'device': device.pk,
            'name': 'Interface [4-6]',
            'type': InterfaceTypeChoices.TYPE_OTHER,
            'enabled': False,
            'bridge': interfaces[4].pk,
            'lag': interfaces[3].pk,
            'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
            'mtu': 2000,
            'speed': 16_000_000_000,
            'duplex': 'half',
            'mgmt_only': True,
            'description': 'A front port',
            'poe_mode': InterfacePoEModeChoices.MODE_PSE,
            'poe_type': InterfacePoETypeChoices.TYPE_1_8023AF,
            'mode': InterfaceModeChoices.MODE_TAGGED,
            'untagged_vlan': vlans[0].pk,
            'tagged_vlans': [v.pk for v in vlans[1:4]],
            'wireless_lans': [wireless_lans[0].pk, wireless_lans[1].pk],
            'vrf': vrfs[0].pk,
            'tags': [t.pk for t in tags],
        }

        cls.bulk_edit_data = {
            'type': InterfaceTypeChoices.TYPE_1GE_FIXED,
            'enabled': True,
            'lag': interfaces[3].pk,
            'wwn': EUI('01:02:03:04:05:06:07:08', version=64),
            'mtu': 2000,
            'speed': 1000000,
            'duplex': 'full',
            'mgmt_only': True,
            'description': 'New description',
            'poe_mode': InterfacePoEModeChoices.MODE_PD,
            'poe_type': InterfacePoETypeChoices.TYPE_2_8023AT,
            'mode': InterfaceModeChoices.MODE_TAGGED,
            'tx_power': 10,
            'untagged_vlan': vlans[0].pk,
            'tagged_vlans': [v.pk for v in vlans[1:4]],
            'vrf': vrfs[1].pk,
        }

        cls.csv_data = (
            "device,name,type,vrf.pk,poe_mode,poe_type,mode,untagged_vlan,tagged_vlans",
            (
                f"Device 1,Interface 4,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af,"
                f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
            ),
            (
                f"Device 1,Interface 5,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af,"
                f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
            ),
            (
                f"Device 1,Interface 6,1000base-t,{vrfs[0].pk},pse,type1-ieee802.3af,"
                f"tagged,{vlans[0].vid},'{','.join([str(v.vid) for v in vlans[1:4]])}'"
            ),
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{interfaces[0].pk},Interface 7,New description7",
            f"{interfaces[1].pk},Interface 8,New description8",
            f"{interfaces[2].pk},Interface 9,New description9",
        )

    def test_trace(self):
        self.add_permissions(
            'dcim.view_interface',
            'dcim.view_cable',
            'dcim.view_device',
        )
        interface1, interface2 = Interface.objects.all()[:2]
        Cable(a_terminations=[interface1], b_terminations=[interface2]).save()

        response = self.client.get(reverse('dcim:interface_trace', kwargs={'pk': interface1.pk}))
        self.assertHttpStatus(response, 200)

    def test_bulk_delete_child_interfaces(self):
        interface1 = Interface.objects.get(name='Interface 1')
        device = interface1.device
        self.add_permissions('dcim.delete_interface')

        # Create a child interface
        child = Interface.objects.create(
            device=device,
            name='Interface 1A',
            type=InterfaceTypeChoices.TYPE_VIRTUAL,
            parent=interface1
        )
        self.assertEqual(device.interfaces.count(), 6)

        # Attempt to delete only the parent interface
        data = {
            'confirm': True,
        }
        self.client.post(self._get_url('delete', interface1), data)
        self.assertEqual(device.interfaces.count(), 6)  # Parent was not deleted

        # Attempt to bulk delete parent & child together
        data = {
            'pk': [interface1.pk, child.pk],
            'confirm': True,
            '_confirm': True,  # Form button
        }
        self.client.post(self._get_url('bulk_delete'), data)
        self.assertEqual(device.interfaces.count(), 4)  # Child & parent were both deleted

    def test_rename_select_all_spans_pages(self):
        """
        Tests the bulk rename functionality for interfaces spanning multiple pages in the UI.
        """
        device_name = 'DeviceRename'
        device = create_test_device(device_name)
        # Create > default page size (25) so selection spans multiple pages
        for i in range(37):
            Interface.objects.create(device=device, name=f'eth{i}')

        self.add_permissions('dcim.change_interface')

        # Filter to this device's interfaces to simulate a real list filter
        get_qs = {'device_id': Device.objects.get(name=device_name).pk}
        post_url = f'{self._get_url("bulk_rename")}?device_id={get_qs["device_id"]}'

        # Preview step: ensure 37 selected (not just one page)
        data = {'_preview': '1', '_all': '1', 'find': 'eth', 'replace': 'xe', 'field_names': ['name']}
        response = self.client.post(post_url, data=data)
        self.assertHttpStatus(response, 200)
        self.assertEqual(len(response.context['selected_objects']), 37)

        # Extract pk[] just like the browser would submit on Apply
        # (either from the form's initial, or from selected_objects)
        pk_list = response.context['form'].initial.get('pk')
        if not pk_list:
            pk_list = [obj.pk for obj in response.context['selected_objects']]
        pk_list = [str(pk) for pk in pk_list]

        # Apply step: include pk[] in the POST
        apply_data = {
            '_apply': '1', '_all': '1', 'find': 'eth', 'replace': 'xe', 'pk': pk_list, 'field_names': ['name'],
        }
        response = self.client.post(post_url, data=apply_data)

        # On success the view redirects back to the return URL
        self.assertHttpStatus(response, 302)
        self.assertEqual(Interface.objects.filter(device=device, name__startswith='xe').count(), 37)


class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
    model = FrontPort
    validation_excluded_fields = ('name', 'label', 'rear_port')

    @classmethod
    def setUpTestData(cls):
        device = create_test_device('Device 1')

        rear_ports = (
            RearPort(device=device, name='Rear Port 1'),
            RearPort(device=device, name='Rear Port 2'),
            RearPort(device=device, name='Rear Port 3'),
            RearPort(device=device, name='Rear Port 4'),
            RearPort(device=device, name='Rear Port 5'),
            RearPort(device=device, name='Rear Port 6'),
        )
        RearPort.objects.bulk_create(rear_ports)

        front_ports = (
            FrontPort(device=device, name='Front Port 1'),
            FrontPort(device=device, name='Front Port 2'),
            FrontPort(device=device, name='Front Port 3'),
        )
        FrontPort.objects.bulk_create(front_ports)
        PortMapping.objects.bulk_create([
            PortMapping(device=device, front_port=front_ports[0], rear_port=rear_ports[0]),
            PortMapping(device=device, front_port=front_ports[1], rear_port=rear_ports[1]),
            PortMapping(device=device, front_port=front_ports[2], rear_port=rear_ports[2]),
        ])

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'device': device.pk,
            'name': 'Front Port X',
            'type': PortTypeChoices.TYPE_8P8C,
            'positions': 1,
            'rear_ports': [f'{rear_ports[3].pk}:1'],
            'description': 'New description',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_create_data = {
            'device': device.pk,
            'name': 'Front Port [4-6]',
            'type': PortTypeChoices.TYPE_8P8C,
            'positions': 1,
            'rear_ports': [f'{rp.pk}:1' for rp in rear_ports[3:6]],
            'description': 'New description',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_edit_data = {
            'type': PortTypeChoices.TYPE_8P8C,
            'description': 'New description',
        }

        cls.csv_data = (
            "device,name,type,positions,rear_port,rear_port_position",
            "Device 1,Front Port 4,8p8c,1,Rear Port 4,1",
            "Device 1,Front Port 5,8p8c,1,Rear Port 5,1",
            "Device 1,Front Port 6,8p8c,1,Rear Port 6,1",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{front_ports[0].pk},Front Port 7,New description7",
            f"{front_ports[1].pk},Front Port 8,New description8",
            f"{front_ports[2].pk},Front Port 9,New description9",
        )

    def test_bulk_import_objects_with_permission(self):
        # Importing front ports with a rear_port (and position) should create the corresponding PortMapping
        def check_port_mappings(scenario_name):
            front_port = FrontPort.objects.get(name='Front Port 4')
            mapping = PortMapping.objects.get(front_port=front_port)
            self.assertEqual(mapping.rear_port.name, 'Rear Port 4')
            self.assertEqual(mapping.front_port_position, 1)
            self.assertEqual(mapping.rear_port_position, 1)

        super().test_bulk_import_objects_with_permission(post_import_callback=check_port_mappings)

    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
    def test_bulk_import_rear_port_position_exceeds_capacity(self):
        # A rear_port_position beyond the rear port's capacity is rejected without creating the front port
        self.add_permissions('dcim.add_frontport')
        csv_data = (
            "device,name,type,positions,rear_port,rear_port_position",
            "Device 1,Front Port 10,8p8c,1,Rear Port 4,2",
        )
        response = self.client.post(self._get_url('bulk_import'), {
            'data': '\n'.join(csv_data),
            'format': ImportFormatChoices.CSV,
            'csv_delimiter': CSVDelimiterChoices.AUTO,
        })
        self.assertEqual(response.status_code, 200)
        self.assertFalse(FrontPort.objects.filter(name='Front Port 10').exists())

    @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
    def test_bulk_import_rear_port_position_occupied(self):
        # An already-occupied rear port position is rejected (rather than raising an IntegrityError)
        self.add_permissions('dcim.add_frontport')
        csv_data = (
            "device,name,type,positions,rear_port,rear_port_position",
            "Device 1,Front Port 10,8p8c,1,Rear Port 1,1",
        )
        response = self.client.post(self._get_url('bulk_import'), {
            'data': '\n'.join(csv_data),
            'format': ImportFormatChoices.CSV,
            'csv_delimiter': CSVDelimiterChoices.AUTO,
        })
        self.assertEqual(response.status_code, 200)
        self.assertFalse(FrontPort.objects.filter(name='Front Port 10').exists())

    def test_trace(self):
        self.add_permissions(
            'dcim.view_frontport',
            'dcim.view_rearport',
            'dcim.view_interface',
            'dcim.view_cable',
            'dcim.view_device',
        )
        frontport = FrontPort.objects.first()
        interface = Interface.objects.create(
            device=frontport.device,
            name='Interface 1'
        )
        Cable(a_terminations=[frontport], b_terminations=[interface]).save()

        response = self.client.get(reverse('dcim:frontport_trace', kwargs={'pk': frontport.pk}))
        self.assertHttpStatus(response, 200)


class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
    model = RearPort
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        device = create_test_device('Device 1')

        rear_ports = (
            RearPort(device=device, name='Rear Port 1'),
            RearPort(device=device, name='Rear Port 2'),
            RearPort(device=device, name='Rear Port 3'),
        )
        RearPort.objects.bulk_create(rear_ports)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'device': device.pk,
            'name': 'Rear Port X',
            'type': PortTypeChoices.TYPE_8P8C,
            'positions': 3,
            'description': 'A rear port',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_create_data = {
            'device': device.pk,
            'name': 'Rear Port [4-6]',
            'type': PortTypeChoices.TYPE_8P8C,
            'positions': 3,
            'description': 'A rear port',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_edit_data = {
            'type': PortTypeChoices.TYPE_8P8C,
            'description': 'New description',
        }

        cls.csv_data = (
            "device,name,type,positions",
            "Device 1,Rear Port 4,8p8c,1",
            "Device 1,Rear Port 5,8p8c,1",
            "Device 1,Rear Port 6,8p8c,1",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{rear_ports[0].pk},Rear Port 7,New description7",
            f"{rear_ports[1].pk},Rear Port 8,New description8",
            f"{rear_ports[2].pk},Rear Port 9,New description9",
        )

    def test_trace(self):
        self.add_permissions(
            'dcim.view_rearport',
            'dcim.view_frontport',
            'dcim.view_interface',
            'dcim.view_cable',
            'dcim.view_device',
        )
        rearport = RearPort.objects.first()
        interface = Interface.objects.create(
            device=rearport.device,
            name='Interface 1'
        )
        Cable(a_terminations=[rearport], b_terminations=[interface]).save()

        response = self.client.get(reverse('dcim:rearport_trace', kwargs={'pk': rearport.pk}))
        self.assertHttpStatus(response, 200)


class ModuleBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
    model = ModuleBay
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        device = create_test_device('Device 1')

        module_bays = (
            ModuleBay(device=device, name='Module Bay 1'),
            ModuleBay(device=device, name='Module Bay 2'),
            ModuleBay(device=device, name='Module Bay 3'),
        )
        for module_bay in module_bays:
            module_bay.save()

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'device': device.pk,
            'name': 'Module Bay X',
            'description': 'A device bay',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_create_data = {
            'device': device.pk,
            'name': 'Module Bay [4-6]',
            'description': 'A module bay',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_edit_data = {
            'description': 'New description',
        }

        cls.csv_data = (
            "device,name",
            "Device 1,Module Bay 4",
            "Device 1,Module Bay 5",
            "Device 1,Module Bay 6",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{module_bays[0].pk},Module Bay 7,New description7",
            f"{module_bays[1].pk},Module Bay 8,New description8",
            f"{module_bays[2].pk},Module Bay 9,New description9",
        )

    @tag('regression')  # Issue #22773
    def test_bulk_add_module_bays_to_devices(self):
        """
        Bulk-adding module bays expands the name pattern per device and applies enabled to every new bay.
        """
        self.add_permissions('dcim.add_modulebay')
        device1 = Device.objects.get(name='Device 1')
        device2 = create_test_device('Device 2')
        initial_count = self._get_queryset().count()

        # An unchecked box is not submitted by the browser at all
        request = {
            'path': reverse('dcim:device_bulk_add_modulebay'),
            'data': post_data({
                'pk': [device1.pk, device2.pk],
                'name': 'PCI-Slot[1-2]',
                '_create': True,
            }),
        }
        response = self.client.post(**request)
        self.assertHttpStatus(response, 302)
        self.assertEqual(
            list(
                ModuleBay.objects.filter(name__startswith='PCI-Slot')
                .order_by('device_id', 'name')
                .values_list('device_id', 'name', 'enabled')
            ),
            [
                (device1.pk, 'PCI-Slot1', False),
                (device1.pk, 'PCI-Slot2', False),
                (device2.pk, 'PCI-Slot1', False),
                (device2.pk, 'PCI-Slot2', False),
            ]
        )

        # A checked box applies True to every bay created
        request = {
            'path': reverse('dcim:device_bulk_add_modulebay'),
            'data': post_data({
                'pk': [device1.pk, device2.pk],
                'name': 'PSU-Slot[1-2]',
                'enabled': True,
                '_create': True,
            }),
        }
        response = self.client.post(**request)
        self.assertHttpStatus(response, 302)
        self.assertEqual(
            list(
                ModuleBay.objects.filter(name__startswith='PSU-Slot')
                .order_by('device_id', 'name')
                .values_list('device_id', 'name', 'enabled')
            ),
            [
                (device1.pk, 'PSU-Slot1', True),
                (device1.pk, 'PSU-Slot2', True),
                (device2.pk, 'PSU-Slot1', True),
                (device2.pk, 'PSU-Slot2', True),
            ]
        )

        self.assertEqual(initial_count + 8, self._get_queryset().count())


class DeviceBayTestCase(ViewTestCases.DeviceComponentViewTestCase):
    model = DeviceBay
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        device = create_test_device('Device 1')

        # Update the DeviceType subdevice role to allow adding DeviceBays
        DeviceType.objects.update(subdevice_role=SubdeviceRoleChoices.ROLE_PARENT)

        device_bays = (
            DeviceBay(device=device, name='Device Bay 1'),
            DeviceBay(device=device, name='Device Bay 2'),
            DeviceBay(device=device, name='Device Bay 3'),
        )
        DeviceBay.objects.bulk_create(device_bays)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'device': device.pk,
            'name': 'Device Bay X',
            'description': 'A device bay',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_create_data = {
            'device': device.pk,
            'name': 'Device Bay [4-6]',
            'description': 'A device bay',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_edit_data = {
            'description': 'New description',
        }

        cls.csv_data = (
            "device,name",
            "Device 1,Device Bay 4",
            "Device 1,Device Bay 5",
            "Device 1,Device Bay 6",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{device_bays[0].pk},Device Bay 7,New description7",
            f"{device_bays[1].pk},Device Bay 8,New description8",
            f"{device_bays[2].pk},Device Bay 9,New description9",
        )


class InventoryItemTestCase(ViewTestCases.DeviceComponentViewTestCase):
    model = InventoryItem
    validation_excluded_fields = ('name', 'label')

    @classmethod
    def setUpTestData(cls):
        device = create_test_device('Device 1')
        manufacturer, _ = Manufacturer.objects.get_or_create(name='Manufacturer 1', slug='manufacturer-1')

        roles = (
            InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1'),
            InventoryItemRole(name='Inventory Item Role 2', slug='inventory-item-role-2'),
        )
        InventoryItemRole.objects.bulk_create(roles)

        inventory_item1 = InventoryItem.objects.create(
            device=device, name='Inventory Item 1', role=roles[0], manufacturer=manufacturer
        )
        inventory_item2 = InventoryItem.objects.create(
            device=device, name='Inventory Item 2', role=roles[0], manufacturer=manufacturer
        )
        inventory_item3 = InventoryItem.objects.create(
            device=device, name='Inventory Item 3', role=roles[0], manufacturer=manufacturer
        )

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'device': device.pk,
            'role': roles[1].pk,
            'manufacturer': manufacturer.pk,
            'name': 'Inventory Item X',
            'parent': None,
            'discovered': False,
            'part_id': '123456',
            'serial': '123ABC',
            'asset_tag': 'ABC123',
            'status': InventoryItemStatusChoices.STATUS_ACTIVE,
            'description': 'An inventory item',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_create_data = {
            'device': device.pk,
            'name': 'Inventory Item [4-6]',
            'role': roles[1].pk,
            'manufacturer': manufacturer.pk,
            'parent': None,
            'discovered': False,
            'part_id': '123456',
            'serial': '123ABC',
            'status': InventoryItemStatusChoices.STATUS_ACTIVE,
            'description': 'An inventory item',
            'tags': [t.pk for t in tags],
        }

        cls.bulk_edit_data = {
            'role': roles[1].pk,
            'part_id': '123456',
            'description': 'New description',
        }

        cls.csv_data = (
            "device,name,parent,status",
            "Device 1,Inventory Item 4,Inventory Item 1,active",
            "Device 1,Inventory Item 5,Inventory Item 2,planned",
            "Device 1,Inventory Item 6,Inventory Item 3,failed",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{inventory_item1.pk},Inventory Item 7,New description7",
            f"{inventory_item2.pk},Inventory Item 8,New description8",
            f"{inventory_item3.pk},Inventory Item 9,New description9",
        )


class InventoryItemRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
    model = InventoryItemRole

    @classmethod
    def setUpTestData(cls):

        inventory_item_roles = (
            InventoryItemRole(name='Inventory Item Role 1', slug='inventory-item-role-1'),
            InventoryItemRole(name='Inventory Item Role 2', slug='inventory-item-role-2'),
            InventoryItemRole(name='Inventory Item Role 3', slug='inventory-item-role-3'),
        )
        InventoryItemRole.objects.bulk_create(inventory_item_roles)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Inventory Item Role X',
            'slug': 'inventory-item-role-x',
            'color': 'c0c0c0',
            'description': 'New inventory item role',
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "name,slug,color",
            "Inventory Item Role 4,inventory-item-role-4,ff0000",
            "Inventory Item Role 5,inventory-item-role-5,00ff00",
            "Inventory Item Role 6,inventory-item-role-6,0000ff",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{inventory_item_roles[0].pk},Inventory Item Role 7,New description7",
            f"{inventory_item_roles[1].pk},Inventory Item Role 8,New description8",
            f"{inventory_item_roles[2].pk},Inventory Item Role 9,New description9",
        )

        cls.bulk_edit_data = {
            'color': '00ff00',
            'description': 'New description',
        }


class CableBundleTestCase(ViewTestCases.PrimaryObjectViewTestCase):
    model = CableBundle

    @classmethod
    def setUpTestData(cls):
        cable_bundles = (
            CableBundle(name='Cable Bundle 1'),
            CableBundle(name='Cable Bundle 2'),
            CableBundle(name='Cable Bundle 3'),
        )
        CableBundle.objects.bulk_create(cable_bundles)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Cable Bundle X',
            'description': 'A test bundle',
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "name,description",
            "Cable Bundle 4,Fourth bundle",
            "Cable Bundle 5,Fifth bundle",
            "Cable Bundle 6,",
        )

        cls.csv_update_data = (
            "id,name,description",
            f"{cable_bundles[0].pk},Cable Bundle 7,New description7",
            f"{cable_bundles[1].pk},Cable Bundle 8,New description8",
            f"{cable_bundles[2].pk},Cable Bundle 9,New description9",
        )

        cls.bulk_edit_data = {
            'description': 'New description',
        }


# TODO: Change base class to PrimaryObjectViewTestCase
# Blocked by lack of common creation view for cables (termination A must be initialized)
class CableTestCase(
    ViewTestCases.GetObjectViewTestCase,
    ViewTestCases.GetObjectChangelogViewTestCase,
    ViewTestCases.EditObjectViewTestCase,
    ViewTestCases.DeleteObjectViewTestCase,
    ViewTestCases.ListObjectsViewTestCase,
    ViewTestCases.BulkImportObjectsViewTestCase,
    ViewTestCases.BulkEditObjectsViewTestCase,
    ViewTestCases.BulkDeleteObjectsViewTestCase
):
    model = Cable

    @classmethod
    def setUpTestData(cls):

        sites = (
            Site(name='Site 1', slug='site-1'),
            Site(name='Site 2', slug='site-2'),
        )
        Site.objects.bulk_create(sites)
        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
        devicetype = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
        role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
        vc = VirtualChassis.objects.create(name='Virtual Chassis')

        # NOTE: By design, NetBox now allows for the creation of devices with the same name if they belong to
        # different sites.
        # The CSV test below demonstrates that devices with identical names on different sites can be created
        # and referenced successfully.
        devices = (
            # Create 'Device 1' assigned to 'Site 1'
            Device(name='Device 1', site=sites[0], device_type=devicetype, role=role),
            Device(name='Device 2', site=sites[0], device_type=devicetype, role=role),
            Device(name='Device 3', site=sites[0], device_type=devicetype, role=role),
            Device(name='Device 4', site=sites[0], device_type=devicetype, role=role),
            # Create 'Device 1' assigned to 'Site 2' (allowed since the site is different)
            Device(name='Device 1', site=sites[1], device_type=devicetype, role=role),
            Device(name='Device 5', site=sites[1], device_type=devicetype, role=role),
        )
        Device.objects.bulk_create(devices)

        vc.members.set((devices[0], devices[1], devices[2]))
        vc.master = devices[0]
        vc.save()

        interfaces = (
            # Device 1, Site 1
            Interface(device=devices[0], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[0], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[0], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            # Device 2, Site 1
            Interface(device=devices[1], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[1], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[1], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            # Device 3, Site 1
            Interface(device=devices[2], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[2], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[2], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            # Device 3, Site 1
            Interface(device=devices[3], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[3], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[3], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            # Device 1, Site 2
            Interface(device=devices[4], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[4], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[4], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),

            # Device 1, Site 2
            Interface(device=devices[5], name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[5], name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[5], name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),

            Interface(device=devices[1], name='Device 2 Interface', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[2], name='Device 3 Interface', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[4], name='Interface 4', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=devices[4], name='Interface 5', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
        )
        Interface.objects.bulk_create(interfaces)

        cable1 = Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[3]], type=CableTypeChoices.TYPE_CAT6)
        cable1.save()
        cable2 = Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[4]], type=CableTypeChoices.TYPE_CAT6)
        cable2.save()
        cable3 = Cable(a_terminations=[interfaces[2]], b_terminations=[interfaces[5]], type=CableTypeChoices.TYPE_CAT6)
        cable3.save()

        # Power panel, power feeds, and power ports for powerfeed-to-powerport cable import tests
        power_panel = PowerPanel.objects.create(site=sites[0], name='Power Panel 1')
        power_feeds = (
            PowerFeed(name='Power Feed 1', power_panel=power_panel),
            PowerFeed(name='Power Feed 2', power_panel=power_panel),
            PowerFeed(name='Power Feed 3', power_panel=power_panel),
        )
        PowerFeed.objects.bulk_create(power_feeds)
        power_ports = (
            PowerPort(device=devices[3], name='Power Port 1'),
            PowerPort(device=devices[3], name='Power Port 2'),
            PowerPort(device=devices[3], name='Power Port 3'),
        )
        PowerPort.objects.bulk_create(power_ports)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            # TODO: Revisit this limitation
            # Changing terminations not supported when editing an existing Cable
            'a_terminations': [interfaces[0].pk],
            'b_terminations': [interfaces[3].pk],
            'type': CableTypeChoices.TYPE_CAT6,
            'status': LinkStatusChoices.STATUS_PLANNED,
            'label': 'Label',
            'color': 'c0c0c0',
            'length': 100,
            'length_unit': CableLengthUnitChoices.UNIT_FOOT,
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = {
            'default': (
                "side_a_device,side_a_type,side_a_name,side_b_device,side_b_type,side_b_name",
                "Device 4,dcim.interface,Interface 1,Device 5,dcim.interface,Interface 1",
                "Device 3,dcim.interface,Interface 2,Device 4,dcim.interface,Interface 2",
                "Device 3,dcim.interface,Interface 3,Device 4,dcim.interface,Interface 3",

                # The following is no longer possible in this scenario, because there are multiple
                # devices named "Device 1" across multiple sites. See the "site-filtering" scenario
                # below for how to specify a site for non-unique device names.
                # "Device 1,dcim.interface,Device 3 Interface,Device 4,dcim.interface,Interface 5",
            ),
            'site-filtering': (
                # Ensure that CSV bulk import supports assigning terminations from parent devices
                # that share the same device name, provided those devices belong to different sites.
                "side_a_site,side_a_device,side_a_type,side_a_name,side_b_site,side_b_device,side_b_type,side_b_name",
                "Site 1,Device 3,dcim.interface,Interface 1,Site 2,Device 1,dcim.interface,Interface 1",
                "Site 1,Device 3,dcim.interface,Interface 2,Site 2,Device 1,dcim.interface,Interface 2",
                "Site 1,Device 3,dcim.interface,Interface 3,Site 2,Device 1,dcim.interface,Interface 3",
                "Site 1,Device 1,dcim.interface,Device 2 Interface,Site 2,Device 1,dcim.interface,Interface 4",
                "Site 1,Device 1,dcim.interface,Device 3 Interface,Site 2,Device 1,dcim.interface,Interface 5",
            ),
            'powerfeed-to-powerport': (
                # Ensure that powerfeed-to-powerport cables can be imported via CSV using side_a_power_panel
                "side_a_power_panel,side_a_type,side_a_name,side_b_device,side_b_type,side_b_name",
                "Power Panel 1,dcim.powerfeed,Power Feed 1,Device 4,dcim.powerport,Power Port 1",
                "Power Panel 1,dcim.powerfeed,Power Feed 2,Device 4,dcim.powerport,Power Port 2",
                "Power Panel 1,dcim.powerfeed,Power Feed 3,Device 4,dcim.powerport,Power Port 3",
            ),
        }

        cls.csv_update_data = (
            "id,label,color",
            f"{cable1.pk},New label7,00ff00",
            f"{cable2.pk},New label8,00ff00",
            f"{cable3.pk},New label9,00ff00",
        )

        cls.bulk_edit_data = {
            'type': CableTypeChoices.TYPE_CAT5E,
            'status': LinkStatusChoices.STATUS_CONNECTED,
            'label': 'New label',
            'color': '00ff00',
            'length': 50,
            'length_unit': CableLengthUnitChoices.UNIT_METER,
        }

    def model_to_dict(self, *args, **kwargs):
        data = super().model_to_dict(*args, **kwargs)

        # Serialize termination objects
        if 'a_terminations' in data:
            data['a_terminations'] = [obj.pk for obj in data['a_terminations']]
        if 'b_terminations' in data:
            data['b_terminations'] = [obj.pk for obj in data['b_terminations']]

        return data


#
# Connections
#

class ConnectionsListViewTestCaseMixin:
    """
    Shared behavior for the read-only connection list views.

    These views list components whose cable paths are complete, but their URL names
    do not follow the <model>_list pattern assumed by ModelViewTestCase.
    """
    url_base = None

    def _get_base_url(self):
        return self.url_base

    def _get_queryset(self):
        return self.model.objects.filter(_path__is_complete=True)


class ConsoleConnectionsListViewTestCase(
    ConnectionsListViewTestCaseMixin,
    ViewTestCases.ListObjectsViewTestCase
):
    model = ConsolePort
    url_base = 'dcim:console_connections_{}'
    query_count_model_label = 'consoleconnection'

    @classmethod
    def setUpTestData(cls):
        device = create_test_device('Device 1')
        peer_device = create_test_device('Device 2')

        console_ports = ConsolePort.objects.bulk_create((
            ConsolePort(device=device, name='Console Port 1'),
            ConsolePort(device=device, name='Console Port 2'),
            ConsolePort(device=device, name='Console Port 3'),
        ))
        console_server_ports = ConsoleServerPort.objects.bulk_create((
            ConsoleServerPort(device=peer_device, name='Console Server Port 1'),
            ConsoleServerPort(device=peer_device, name='Console Server Port 2'),
            ConsoleServerPort(device=peer_device, name='Console Server Port 3'),
        ))

        for console_port, console_server_port in zip(console_ports, console_server_ports):
            Cable(a_terminations=[console_port], b_terminations=[console_server_port]).save()


class PowerConnectionsListViewTestCase(
    ConnectionsListViewTestCaseMixin,
    ViewTestCases.ListObjectsViewTestCase
):
    model = PowerPort
    url_base = 'dcim:power_connections_{}'
    query_count_model_label = 'powerconnection'

    @classmethod
    def setUpTestData(cls):
        device = create_test_device('Device 1')
        peer_device = create_test_device('Device 2')

        power_ports = PowerPort.objects.bulk_create((
            PowerPort(device=device, name='Power Port 1'),
            PowerPort(device=device, name='Power Port 2'),
            PowerPort(device=device, name='Power Port 3'),
        ))
        power_outlets = PowerOutlet.objects.bulk_create((
            PowerOutlet(device=peer_device, name='Power Outlet 1'),
            PowerOutlet(device=peer_device, name='Power Outlet 2'),
            PowerOutlet(device=peer_device, name='Power Outlet 3'),
        ))

        for power_port, power_outlet in zip(power_ports, power_outlets):
            Cable(a_terminations=[power_port], b_terminations=[power_outlet]).save()


class InterfaceConnectionsListViewTestCase(
    ConnectionsListViewTestCaseMixin,
    ViewTestCases.ListObjectsViewTestCase
):
    model = Interface
    url_base = 'dcim:interface_connections_{}'
    query_count_model_label = 'interfaceconnection'

    @classmethod
    def setUpTestData(cls):
        device = create_test_device('Device 1')
        peer_device = create_test_device('Device 2')

        interfaces = Interface.objects.bulk_create((
            Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
        ))
        peer_interfaces = Interface.objects.bulk_create((
            Interface(device=peer_device, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=peer_device, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
            Interface(device=peer_device, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
        ))

        for interface, peer_interface in zip(interfaces, peer_interfaces):
            Cable(a_terminations=[interface], b_terminations=[peer_interface]).save()


class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
    model = VirtualChassis

    @classmethod
    def setUpTestData(cls):

        site = Site.objects.create(name='Site 1', slug='site-1')
        manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1')
        device_type = DeviceType.objects.create(
            manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
        )
        role = DeviceRole.objects.create(
            name='Device Role', slug='device-role-1'
        )

        devices = (
            Device(device_type=device_type, role=role, name='Device 1', site=site),
            Device(device_type=device_type, role=role, name='Device 2', site=site),
            Device(device_type=device_type, role=role, name='Device 3', site=site),
            Device(device_type=device_type, role=role, name='Device 4', site=site),
            Device(device_type=device_type, role=role, name='Device 5', site=site),
            Device(device_type=device_type, role=role, name='Device 6', site=site),
            Device(device_type=device_type, role=role, name='Device 7', site=site),
            Device(device_type=device_type, role=role, name='Device 8', site=site),
            Device(device_type=device_type, role=role, name='Device 9', site=site),
            Device(device_type=device_type, role=role, name='Device 10', site=site),
            Device(device_type=device_type, role=role, name='Device 11', site=site),
            Device(device_type=device_type, role=role, name='Device 12', site=site),
        )
        Device.objects.bulk_create(devices)

        # Create three VirtualChassis with three members each
        vc1 = VirtualChassis.objects.create(name='VC1', master=devices[0], domain='domain-1')
        Device.objects.filter(pk=devices[0].pk).update(virtual_chassis=vc1, vc_position=1)
        Device.objects.filter(pk=devices[1].pk).update(virtual_chassis=vc1, vc_position=2)
        Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=vc1, vc_position=3)
        vc2 = VirtualChassis.objects.create(name='VC2', master=devices[3], domain='domain-2')
        Device.objects.filter(pk=devices[3].pk).update(virtual_chassis=vc2, vc_position=1)
        Device.objects.filter(pk=devices[4].pk).update(virtual_chassis=vc2, vc_position=2)
        Device.objects.filter(pk=devices[5].pk).update(virtual_chassis=vc2, vc_position=3)
        vc3 = VirtualChassis.objects.create(name='VC3', master=devices[6], domain='domain-3')
        Device.objects.filter(pk=devices[6].pk).update(virtual_chassis=vc3, vc_position=1)
        Device.objects.filter(pk=devices[7].pk).update(virtual_chassis=vc3, vc_position=2)
        Device.objects.filter(pk=devices[8].pk).update(virtual_chassis=vc3, vc_position=3)

        cls.form_data = {
            'name': 'VC4',
            'domain': 'domain-4',
            # Management form data for VC members
            'form-TOTAL_FORMS': 0,
            'form-INITIAL_FORMS': 3,
            'form-MIN_NUM_FORMS': 0,
            'form-MAX_NUM_FORMS': 1000,
        }

        cls.csv_data = (
            "name,domain,master",
            "VC4,Domain 4,Device 10",
            "VC5,Domain 5,Device 11",
            "VC6,Domain 6,Device 12",
        )

        cls.csv_update_data = (
            "id,name,domain",
            f"{vc1.pk},VC7,Domain 7",
            f"{vc2.pk},VC8,Domain 8",
            f"{vc3.pk},VC9,Domain 9",
        )

        cls.bulk_edit_data = {
            'domain': 'domain-x',
        }


class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
    model = PowerPanel

    @classmethod
    def setUpTestData(cls):

        sites = (
            Site(name='Site 1', slug='site-1'),
            Site(name='Site 2', slug='site-2'),
        )
        Site.objects.bulk_create(sites)

        locations = (
            Location(name='Location 1', slug='location-1', site=sites[0]),
            Location(name='Location 2', slug='location-2', site=sites[1]),
        )
        for location in locations:
            location.save()

        power_panels = (
            PowerPanel(site=sites[0], location=locations[0], name='Power Panel 1'),
            PowerPanel(site=sites[0], location=locations[0], name='Power Panel 2'),
            PowerPanel(site=sites[0], location=locations[0], name='Power Panel 3'),
        )
        PowerPanel.objects.bulk_create(power_panels)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'site': sites[1].pk,
            'location': locations[1].pk,
            'name': 'Power Panel X',
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "site,location,name",
            "Site 1,Location 1,Power Panel 4",
            "Site 1,Location 1,Power Panel 5",
            "Site 1,Location 1,Power Panel 6",
        )

        cls.csv_update_data = (
            "id,name",
            f"{power_panels[0].pk},Power Panel 7",
            f"{power_panels[1].pk},Power Panel 8",
            f"{power_panels[2].pk},Power Panel 9",
        )

        cls.bulk_edit_data = {
            'site': sites[1].pk,
            'location': locations[1].pk,
        }


class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
    model = PowerFeed

    @classmethod
    def setUpTestData(cls):

        site = Site.objects.create(name='Site 1', slug='site-1')

        powerpanels = (
            PowerPanel(site=site, name='Power Panel 1'),
            PowerPanel(site=site, name='Power Panel 2'),
        )
        PowerPanel.objects.bulk_create(powerpanels)

        racks = (
            Rack(site=site, name='Rack 1'),
            Rack(site=site, name='Rack 2'),
        )
        Rack.objects.bulk_create(racks)

        power_feeds = (
            PowerFeed(name='Power Feed 1', power_panel=powerpanels[0], rack=racks[0]),
            PowerFeed(name='Power Feed 2', power_panel=powerpanels[0], rack=racks[0]),
            PowerFeed(name='Power Feed 3', power_panel=powerpanels[0], rack=racks[0]),
        )
        PowerFeed.objects.bulk_create(power_feeds)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'name': 'Power Feed X',
            'power_panel': powerpanels[1].pk,
            'rack': racks[1].pk,
            'status': PowerFeedStatusChoices.STATUS_PLANNED,
            'type': PowerFeedTypeChoices.TYPE_REDUNDANT,
            'supply': PowerFeedSupplyChoices.SUPPLY_DC,
            'phase': PowerFeedPhaseChoices.PHASE_3PHASE,
            'voltage': 100,
            'amperage': 100,
            'max_utilization': 50,
            'comments': 'New comments',
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "site,power_panel,name,status,type,supply,phase,voltage,amperage,max_utilization",
            "Site 1,Power Panel 1,Power Feed 4,active,primary,ac,single-phase,120,20,80",
            "Site 1,Power Panel 1,Power Feed 5,active,primary,ac,single-phase,120,20,80",
            "Site 1,Power Panel 1,Power Feed 6,active,primary,ac,single-phase,120,20,80",
        )

        cls.csv_update_data = (
            "id,name,status",
            f"{power_feeds[0].pk},Power Feed 7,{PowerFeedStatusChoices.STATUS_PLANNED}",
            f"{power_feeds[1].pk},Power Feed 8,{PowerFeedStatusChoices.STATUS_PLANNED}",
            f"{power_feeds[2].pk},Power Feed 9,{PowerFeedStatusChoices.STATUS_PLANNED}",
        )

        cls.bulk_edit_data = {
            'power_panel': powerpanels[1].pk,
            'rack': racks[1].pk,
            'status': PowerFeedStatusChoices.STATUS_PLANNED,
            'type': PowerFeedTypeChoices.TYPE_REDUNDANT,
            'supply': PowerFeedSupplyChoices.SUPPLY_DC,
            'phase': PowerFeedPhaseChoices.PHASE_3PHASE,
            'voltage': 100,
            'amperage': 100,
            'max_utilization': 50,
            'comments': 'New comments',
        }

    def test_trace(self):
        self.add_permissions(
            'dcim.view_powerfeed',
            'dcim.view_powerport',
            'dcim.view_cable',
            'dcim.view_device',
        )
        manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer-1')
        device_type = DeviceType.objects.create(
            manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
        )
        role = DeviceRole.objects.create(
            name='Device Role', slug='device-role-1'
        )
        device = Device.objects.create(
            site=Site.objects.first(), device_type=device_type, role=role
        )

        powerfeed = PowerFeed.objects.first()
        powerport = PowerPort.objects.create(
            device=device,
            name='Power Port 1'
        )
        Cable(a_terminations=[powerfeed], b_terminations=[powerport]).save()

        response = self.client.get(reverse('dcim:powerfeed_trace', kwargs={'pk': powerfeed.pk}))
        self.assertHttpStatus(response, 200)


class VirtualDeviceContextTestCase(ViewTestCases.PrimaryObjectViewTestCase):
    model = VirtualDeviceContext

    @classmethod
    def setUpTestData(cls):
        devices = [create_test_device(name='Device 1')]

        vdcs = (
            VirtualDeviceContext(name='VDC 1', identifier=1, device=devices[0], status='active'),
            VirtualDeviceContext(name='VDC 2', identifier=2, device=devices[0], status='active'),
            VirtualDeviceContext(name='VDC 3', identifier=3, device=devices[0], status='active'),
        )
        VirtualDeviceContext.objects.bulk_create(vdcs)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'device': devices[0].pk,
            'status': 'active',
            'name': 'VDC 4',
            'identifier': 4,
            'primary_ip4': None,
            'primary_ip6': None,
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "device,status,name,identifier",
            "Device 1,active,VDC 5,5",
            "Device 1,active,VDC 6,6",
            "Device 1,active,VDC 7,7",
        )

        cls.csv_update_data = (
            "id,status",
            f"{vdcs[0].pk},{VirtualDeviceContextStatusChoices.STATUS_PLANNED}",
            f"{vdcs[1].pk},{VirtualDeviceContextStatusChoices.STATUS_PLANNED}",
            f"{vdcs[2].pk},{VirtualDeviceContextStatusChoices.STATUS_PLANNED}",
        )

        cls.bulk_edit_data = {
            'status': VirtualDeviceContextStatusChoices.STATUS_OFFLINE,
        }

    def test_bulk_edit_device_context_preserves_device(self):
        """
        Regression test: Bulk editing VDCs from the Device's VDCs tab (URL contains
        ?device=<id>) must not clear the device field on those VDCs.
        """
        self.add_permissions('dcim.view_virtualdevicecontext', 'dcim.change_virtualdevicecontext')

        device = VirtualDeviceContext.objects.filter(device__isnull=False).first().device
        vdcs = list(VirtualDeviceContext.objects.filter(device=device)[:3])
        pk_list = [vdc.pk for vdc in vdcs]

        data = {
            'pk': pk_list,
            '_apply': True,
            # Only change status — device is intentionally omitted
            'status': VirtualDeviceContextStatusChoices.STATUS_PLANNED,
        }

        # Simulate navigation from Device -> VDCs tab by passing ?device=<id> as GET param
        url = reverse('dcim:virtualdevicecontext_bulk_edit') + f'?device={device.pk}'
        response = self.client.post(url, data)
        self.assertHttpStatus(response, 302)

        for vdc in VirtualDeviceContext.objects.filter(pk__in=pk_list):
            self.assertEqual(vdc.device, device, msg=f"Device was unexpectedly cleared on VDC '{vdc.name}'")
            self.assertEqual(vdc.status, VirtualDeviceContextStatusChoices.STATUS_PLANNED)


class MACAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
    model = MACAddress

    @classmethod
    def setUpTestData(cls):
        device = create_test_device(name='Device 1')
        interfaces = (
            Interface(device=device, name='Interface 1', type='1000base-t'),
            Interface(device=device, name='Interface 2', type='1000base-t'),
            Interface(device=device, name='Interface 3', type='1000base-t'),
            Interface(device=device, name='Interface 4', type='1000base-t'),
            Interface(device=device, name='Interface 5', type='1000base-t'),
            Interface(device=device, name='Interface 6', type='1000base-t'),
        )
        Interface.objects.bulk_create(interfaces)

        mac_addresses = (
            MACAddress(mac_address='00:00:00:00:00:01', assigned_object=interfaces[0]),
            MACAddress(mac_address='00:00:00:00:00:02', assigned_object=interfaces[1]),
            MACAddress(mac_address='00:00:00:00:00:03', assigned_object=interfaces[2]),
        )
        MACAddress.objects.bulk_create(mac_addresses)

        tags = create_tags('Alpha', 'Bravo', 'Charlie')

        cls.form_data = {
            'mac_address': EUI('00:00:00:00:00:04'),
            'description': 'New MAC address',
            'interface_id': interfaces[3].pk,
            'tags': [t.pk for t in tags],
        }

        cls.csv_data = (
            "mac_address,device,interface",
            "00:00:00:00:00:04,Device 1,Interface 4",
            "00:00:00:00:00:05,Device 1,Interface 5",
            "00:00:00:00:00:06,Device 1,Interface 6",
        )

        cls.csv_update_data = (
            "id,mac_address",
            f"{mac_addresses[0].pk},00:00:00:00:00:0a",
            f"{mac_addresses[1].pk},00:00:00:00:00:0b",
            f"{mac_addresses[2].pk},00:00:00:00:00:0c",
        )

        cls.bulk_edit_data = {
            'description': 'New description',
        }

    @tag('regression')  # Issue #20542
    def test_create_macaddress_via_quickadd(self):
        """
        Test creating a MAC address via quick-add modal (e.g., from Interface form).
        Regression test for issue #20542 where form prefix was missing in POST handler.
        """
        self.add_permissions('dcim.view_macaddress', 'dcim.view_interface', 'extras.view_tag')
        obj_perm = ObjectPermission(name='Test permission', actions=['add'])
        obj_perm.save()
        obj_perm.users.add(self.user)
        obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))

        # Simulate quick-add form submission with 'quickadd-' prefix
        formatted_data = post_data(self.form_data)
        quickadd_data = {f'quickadd-{k}': v for k, v in formatted_data.items()}
        quickadd_data['_quickadd'] = 'True'

        initial_count = self._get_queryset().count()
        url = f"{self._get_url('add')}?_quickadd=True&target=id_primary_mac_address"
        response = self.client.post(url, data=quickadd_data)

        # Should successfully create the MAC address and return the quick_add_created template
        self.assertHttpStatus(response, 200)
        self.assertIn(b'quick-add-object', response.content)
        self.assertEqual(initial_count + 1, self._get_queryset().count())
