defmodule Towerops.EquipmentTest do
  use Towerops.DataCase
  use ExUnitProperties

  alias Towerops.Devices
  alias Towerops.Devices.Device

  describe "device" do
    import Towerops.AccountsFixtures

    alias Device, as: DeviceSchema

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site, user: user}
    end

    @valid_attrs %{
      name: "Router 1",
      ip_address: "192.168.1.1",
      description: "Main router",
      monitoring_enabled: true,
      check_interval_seconds: 300
    }
    @update_attrs %{
      name: "Updated Router",
      ip_address: "192.168.1.2",
      description: "Updated",
      monitoring_enabled: false
    }
    @invalid_attrs %{name: nil, ip_address: nil}

    test "list_site_devices/1 returns all devices for a site", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(Map.merge(@valid_attrs, %{site_id: site.id, organization_id: organization.id}))

      assert Devices.list_site_devices(site.id) == [device]
    end

    test "list_organization_devices/1 returns all devices for an organization", %{
      organization: organization,
      site: site
    } do
      {:ok, device} =
        Devices.create_device(Map.merge(@valid_attrs, %{site_id: site.id, organization_id: organization.id}))

      result = Devices.list_organization_devices(organization.id)
      assert length(result) == 1
      assert hd(result).id == device.id
    end

    test "list_monitored_devices/0 returns only device with monitoring enabled", %{
      organization: organization,
      site: site
    } do
      {:ok, monitored} =
        Devices.create_device(Map.merge(@valid_attrs, %{site_id: site.id, organization_id: organization.id}))

      {:ok, _not_monitored} =
        Devices.create_device(%{
          name: "Not Monitored",
          ip_address: "192.168.1.3",
          site_id: site.id,
          organization_id: organization.id,
          monitoring_enabled: false
        })

      result = Devices.list_monitored_devices()
      assert length(result) == 1
      assert hd(result).id == monitored.id
    end

    test "get_device!/1 returns the device with given id", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(Map.merge(@valid_attrs, %{site_id: site.id, organization_id: organization.id}))

      assert Devices.get_device!(device.id).id == device.id
    end

    test "get_site_equipment!/2 returns device for specific site", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(Map.merge(@valid_attrs, %{site_id: site.id, organization_id: organization.id}))

      assert Devices.get_site_device!(site.id, device.id).id == device.id
    end

    test "create_device/1 with valid data creates device", %{organization: organization, site: site} do
      attrs = Map.merge(@valid_attrs, %{site_id: site.id, organization_id: organization.id})
      assert {:ok, %DeviceSchema{} = device} = Devices.create_device(attrs)
      assert device.name == "Router 1"
      assert device.ip_address == "192.168.1.1"
      assert device.status == :unknown
      assert device.monitoring_enabled == true
      assert device.check_interval_seconds == 300
    end

    test "create_device/1 with valid IPv6 address", %{organization: organization, site: site} do
      attrs = Map.put(@valid_attrs, :ip_address, "2001:0db8:85a3::8a2e:0370:7334")
      attrs = Map.merge(attrs, %{site_id: site.id, organization_id: organization.id})
      assert {:ok, %DeviceSchema{}} = Devices.create_device(attrs)
    end

    test "create_device/1 with invalid IP address returns error", %{organization: organization, site: site} do
      attrs = Map.put(@valid_attrs, :ip_address, "invalid-ip")
      attrs = Map.merge(attrs, %{site_id: site.id, organization_id: organization.id})
      assert {:error, changeset} = Devices.create_device(attrs)
      assert "must be a valid IPv4 or IPv6 address" in errors_on(changeset).ip_address
    end

    test "create_device/1 with invalid data returns error changeset" do
      assert {:error, %Ecto.Changeset{}} = Devices.create_device(@invalid_attrs)
    end

    test "update_device/2 with valid data updates the device", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(Map.merge(@valid_attrs, %{site_id: site.id, organization_id: organization.id}))

      assert {:ok, %DeviceSchema{} = device} =
               Devices.update_device(device, @update_attrs)

      assert device.name == "Updated Router"
      assert device.ip_address == "192.168.1.2"
      assert device.monitoring_enabled == false
    end

    test "update_device/2 with invalid data returns error changeset", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(Map.merge(@valid_attrs, %{site_id: site.id, organization_id: organization.id}))

      assert {:error, %Ecto.Changeset{}} =
               Devices.update_device(device, @invalid_attrs)

      assert Devices.get_device!(device.id).name == device.name
    end

    test "change_device/1 returns an device changeset", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(Map.merge(@valid_attrs, %{site_id: site.id, organization_id: organization.id}))

      assert %Ecto.Changeset{} = Devices.change_device(device)
    end

    test "update_device_status/2 updates status and timestamps", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(Map.merge(@valid_attrs, %{site_id: site.id, organization_id: organization.id}))

      assert {:ok, updated} = Devices.update_device_status(device, :up)
      assert updated.status == :up
      assert updated.last_checked_at
      assert updated.last_status_change_at
    end

    test "update_device_status/2 only updates last_checked_at if status unchanged", %{
      organization: organization,
      site: site
    } do
      {:ok, device} =
        Devices.create_device(Map.merge(@valid_attrs, %{site_id: site.id, organization_id: organization.id}))

      {:ok, device} = Devices.update_device_status(device, :up)

      # Manually set timestamps to 1 hour ago to avoid sleeping
      # Must truncate to :second to match DB column type
      past_time =
        DateTime.utc_now()
        |> DateTime.add(-3600, :second)
        |> DateTime.truncate(:second)

      {:ok, device} =
        device
        |> Ecto.Changeset.change(%{last_checked_at: past_time, last_status_change_at: past_time})
        |> Towerops.Repo.update()

      first_change_at = device.last_status_change_at
      first_checked_at = device.last_checked_at

      {:ok, updated} = Devices.update_device_status(device, :up)
      # Status change timestamp should not change
      assert updated.last_status_change_at == first_change_at
      # But checked_at should be updated (to now, which is after 1 hour ago)
      assert DateTime.after?(updated.last_checked_at, first_checked_at)
    end

    test "list_snmp_enabled_devices/0 returns only device with SNMP enabled", %{organization: organization, site: site} do
      {:ok, snmp_enabled} =
        Devices.create_device(%{
          name: "SNMP Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          snmp_community: "public"
        })

      {:ok, _snmp_disabled} =
        Devices.create_device(%{
          name: "Non-SNMP Router",
          ip_address: "192.168.1.2",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: false
        })

      result = Devices.list_snmp_enabled_devices()
      assert length(result) == 1
      assert hd(result).id == snmp_enabled.id
    end

    test "update_snmp_poll_time/1 updates last_snmp_poll_at timestamp", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(Map.merge(@valid_attrs, %{site_id: site.id, organization_id: organization.id}))

      assert device.last_snmp_poll_at == nil

      assert {:ok, updated} = Devices.update_snmp_poll_time(device)
      assert updated.last_snmp_poll_at
      assert DateTime.before?(updated.last_snmp_poll_at, DateTime.utc_now())
    end

    test "create_device/1 with SNMP enabled and nil community string succeeds", %{organization: organization, site: site} do
      attrs = %{
        name: "SNMP Router",
        ip_address: "192.168.1.1",
        site_id: site.id,
        organization_id: organization.id,
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_community: nil
      }

      assert {:ok, %DeviceSchema{} = device} = Devices.create_device(attrs)
      assert device.snmp_enabled == true
      assert device.snmp_community == nil
    end

    test "create_device/1 with SNMP enabled and empty community string succeeds", %{
      organization: organization,
      site: site
    } do
      attrs = %{
        name: "SNMP Router",
        ip_address: "192.168.1.1",
        site_id: site.id,
        organization_id: organization.id,
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_community: ""
      }

      # Empty string should be converted to nil by the database
      assert {:ok, %DeviceSchema{} = device} = Devices.create_device(attrs)
      assert device.snmp_enabled == true
      # Empty strings are stored as nil in the database
      assert device.snmp_community == nil || device.snmp_community == ""
    end

    test "reorder_device/2 reorders device to first position", %{organization: organization, site: site} do
      {:ok, device1} =
        Devices.create_device(%{
          name: "Device 1",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, device2} =
        Devices.create_device(%{
          name: "Device 2",
          ip_address: "192.168.1.2",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, device3} =
        Devices.create_device(%{
          name: "Device 3",
          ip_address: "192.168.1.3",
          site_id: site.id,
          organization_id: organization.id
        })

      # Move device3 to first position
      {:ok, _updated} = Devices.reorder_device(device3.id, 1)

      # Verify order
      devices = Devices.list_site_devices(site.id)
      assert Enum.at(devices, 0).id == device3.id
      assert Enum.at(devices, 0).display_order == 1

      assert Enum.at(devices, 1).id == device1.id
      assert Enum.at(devices, 1).display_order == 2

      assert Enum.at(devices, 2).id == device2.id
      assert Enum.at(devices, 2).display_order == 3
    end

    test "reorder_device/2 reorders device to last position", %{organization: organization, site: site} do
      {:ok, device1} =
        Devices.create_device(%{
          name: "Device 1",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, device2} =
        Devices.create_device(%{
          name: "Device 2",
          ip_address: "192.168.1.2",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, device3} =
        Devices.create_device(%{
          name: "Device 3",
          ip_address: "192.168.1.3",
          site_id: site.id,
          organization_id: organization.id
        })

      # Move device1 to last position
      {:ok, _updated} = Devices.reorder_device(device1.id, 3)

      # Verify order
      devices = Devices.list_site_devices(site.id)
      assert Enum.at(devices, 0).id == device2.id
      assert Enum.at(devices, 0).display_order == 1

      assert Enum.at(devices, 1).id == device3.id
      assert Enum.at(devices, 1).display_order == 2

      assert Enum.at(devices, 2).id == device1.id
      assert Enum.at(devices, 2).display_order == 3
    end

    test "reorder_device/2 reorders device to middle position", %{organization: organization, site: site} do
      {:ok, device1} =
        Devices.create_device(%{
          name: "Device 1",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, device2} =
        Devices.create_device(%{
          name: "Device 2",
          ip_address: "192.168.1.2",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, device3} =
        Devices.create_device(%{
          name: "Device 3",
          ip_address: "192.168.1.3",
          site_id: site.id,
          organization_id: organization.id
        })

      # Move device3 to middle position
      {:ok, _updated} = Devices.reorder_device(device3.id, 2)

      # Verify order
      devices = Devices.list_site_devices(site.id)
      assert Enum.at(devices, 0).id == device1.id
      assert Enum.at(devices, 0).display_order == 1

      assert Enum.at(devices, 1).id == device3.id
      assert Enum.at(devices, 1).display_order == 2

      assert Enum.at(devices, 2).id == device2.id
      assert Enum.at(devices, 2).display_order == 3
    end

    test "reorder_device/2 maintains continuous numbering", %{organization: organization, site: site} do
      {:ok, _device1} =
        Devices.create_device(%{
          name: "Device 1",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, device2} =
        Devices.create_device(%{
          name: "Device 2",
          ip_address: "192.168.1.2",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, _device3} =
        Devices.create_device(%{
          name: "Device 3",
          ip_address: "192.168.1.3",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, device4} =
        Devices.create_device(%{
          name: "Device 4",
          ip_address: "192.168.1.4",
          site_id: site.id,
          organization_id: organization.id
        })

      # Reorder multiple times
      {:ok, _} = Devices.reorder_device(device2.id, 1)
      {:ok, _} = Devices.reorder_device(device4.id, 2)

      # Verify all have continuous display_order values
      devices = Devices.list_site_devices(site.id)
      display_orders = Enum.map(devices, & &1.display_order)
      assert display_orders == [1, 2, 3, 4]
    end

    test "reorder_device/2 only affects devices in same site", %{organization: organization, site: site} do
      # Create devices in first site
      {:ok, device1} =
        Devices.create_device(%{
          name: "Device 1",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, device2} =
        Devices.create_device(%{
          name: "Device 2",
          ip_address: "192.168.1.2",
          site_id: site.id,
          organization_id: organization.id
        })

      # Create second site and device
      {:ok, site2} = Towerops.Sites.create_site(%{name: "Site 2", organization_id: organization.id})

      {:ok, device3} =
        Devices.create_device(%{
          name: "Device 3",
          ip_address: "192.168.2.1",
          site_id: site2.id,
          organization_id: organization.id
        })

      # Reorder device in first site
      {:ok, _} = Devices.reorder_device(device1.id, 2)

      # Verify first site is reordered
      site1_devices = Devices.list_site_devices(site.id)
      assert Enum.at(site1_devices, 0).id == device2.id
      assert Enum.at(site1_devices, 1).id == device1.id

      # Verify second site is unchanged (no display_order set)
      site2_devices = Devices.list_site_devices(site2.id)
      assert length(site2_devices) == 1
      assert hd(site2_devices).id == device3.id
      assert is_nil(hd(site2_devices).display_order)
    end

    test "reset_organization_device_order/1 clears all display_order values", %{
      organization: organization,
      site: site
    } do
      {:ok, device1} =
        Devices.create_device(%{
          name: "Zebra Device",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, device2} =
        Devices.create_device(%{
          name: "Alpha Device",
          ip_address: "192.168.1.2",
          site_id: site.id,
          organization_id: organization.id
        })

      # Set custom order
      {:ok, _} = Devices.reorder_device(device1.id, 1)
      {:ok, _} = Devices.reorder_device(device2.id, 2)

      # Verify custom order is set
      devices_before = Devices.list_site_devices(site.id)
      assert List.first(devices_before).id == device1.id
      assert List.first(devices_before).display_order == 1

      # Reset order
      {count, _} = Devices.reset_organization_device_order(organization.id)
      assert count == 2

      # Verify display_order is cleared and falls back to alphabetical
      devices_after = Devices.list_site_devices(site.id)
      assert List.first(devices_after).name == "Alpha Device"
      assert List.first(devices_after).display_order == nil
      assert List.last(devices_after).name == "Zebra Device"
      assert List.last(devices_after).display_order == nil
    end
  end

  describe "SNMP configuration inheritance" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site, user: user}
    end

    test "get_snmp_config/1 returns device-level community string when set", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          snmp_community: "device-community"
        })

      config = Devices.get_snmp_config(device)
      assert config.community == "device-community"
      assert config.source == :device
    end

    test "get_snmp_config/1 returns site-level community when device has nil", %{
      organization: organization,
      site: site
    } do
      # Update site with SNMP community
      {:ok, site} =
        Towerops.Sites.update_site(site, %{
          snmp_community: "site-community",
          snmp_version: "2c"
        })

      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          snmp_community: nil
        })

      device = Towerops.Repo.preload(device, site: :organization)
      config = Devices.get_snmp_config(device)
      assert config.community == "site-community"
      assert config.source == :site
    end

    test "get_snmp_config/1 returns org-level community when device and site have nil", %{
      organization: organization,
      site: site
    } do
      # Update organization with SNMP community
      {:ok, _organization} =
        Towerops.Organizations.update_organization(organization, %{
          snmp_community: "org-community",
          snmp_version: "2c"
        })

      # Reload site with updated organization
      site = Towerops.Repo.preload(site, :organization, force: true)

      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          snmp_community: nil
        })

      device = Towerops.Repo.preload(device, site: :organization)
      config = Devices.get_snmp_config(device)
      assert config.community == "org-community"
      assert config.source == :organization
    end

    test "get_snmp_config/1 returns nil when all levels have nil", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          snmp_community: nil
        })

      device = Towerops.Repo.preload(device, site: :organization)
      config = Devices.get_snmp_config(device)
      assert config.community == nil
      assert config.source == :organization
    end

    test "get_snmp_config/1 prioritizes device over site over org", %{
      organization: organization,
      site: site
    } do
      # Set community at all levels
      {:ok, _organization} =
        Towerops.Organizations.update_organization(organization, %{
          snmp_community: "org-community",
          snmp_version: "2c"
        })

      {:ok, site} =
        Towerops.Sites.update_site(site, %{
          snmp_community: "site-community",
          snmp_version: "2c"
        })

      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          snmp_community: "device-community"
        })

      device = Towerops.Repo.preload(device, site: :organization)
      config = Devices.get_snmp_config(device)
      # Should use device-level community
      assert config.community == "device-community"
      assert config.source == :device
    end
  end

  describe "events" do
    import Towerops.AccountsFixtures

    alias Towerops.Devices.Event

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      {:ok, device} =
        Devices.create_device(%{
          name: "Router 1",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      %{device: device}
    end

    test "create_event/1 creates an device event", %{device: device} do
      attrs = %{
        device_id: device.id,
        event_type: "interface_up",
        severity: "info",
        message: "Interface came online",
        metadata: %{"interface" => "eth0"},
        occurred_at: DateTime.truncate(DateTime.utc_now(), :second)
      }

      assert {:ok, %Event{} = event} = Devices.create_event(attrs)
      assert event.device_id == device.id
      assert event.event_type == "interface_up"
      assert event.severity == "info"
      assert event.message == "Interface came online"
      assert event.metadata == %{"interface" => "eth0"}
    end

    test "create_event/1 with invalid data returns error changeset" do
      assert {:error, %Ecto.Changeset{}} = Devices.create_event(%{})
    end

    test "list_devices_events/2 returns events for device ordered by most recent", %{
      device: device
    } do
      now = DateTime.utc_now()

      {:ok, event1} =
        Devices.create_event(%{
          device_id: device.id,
          event_type: "interface_up",
          severity: "info",
          message: "Interface came up",
          occurred_at: DateTime.add(now, -3600, :second)
        })

      {:ok, event2} =
        Devices.create_event(%{
          device_id: device.id,
          event_type: "interface_down",
          severity: "warning",
          message: "Interface went down",
          occurred_at: DateTime.add(now, -1800, :second)
        })

      events = Devices.list_devices_events(device.id)
      assert length(events) == 2
      # Should be ordered by most recent first
      assert hd(events).id == event2.id
      assert List.last(events).id == event1.id
    end

    test "list_devices_events/2 limits results", %{device: device} do
      now = DateTime.utc_now()

      # Create 10 events
      for i <- 1..10 do
        Devices.create_event(%{
          device_id: device.id,
          event_type: "interface_down",
          severity: "warning",
          message: "Interface down event #{i}",
          occurred_at: DateTime.add(now, -i * 60, :second)
        })
      end

      events = Devices.list_devices_events(device.id, 5)
      assert length(events) == 5
    end
  end

  describe "additional coverage tests" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site, user: user}
    end

    test "get_device/1 returns device when exists", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      found = Devices.get_device(device.id)
      assert found
      assert found.id == device.id
    end

    test "get_device/1 returns nil when not exists" do
      assert Devices.get_device(Ecto.UUID.generate()) == nil
    end

    test "get_device_with_details/1 preloads associations", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      result = Devices.get_device_with_details(device.id)
      assert result.id == device.id
      assert Ecto.assoc_loaded?(result.site)
    end

    @tag :skip
    test "delete_device/1 deletes the device", %{organization: organization, site: site} do
      # Skipped: Requires Monitoring.Supervisor to be running
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      assert {:ok, _deleted} = Devices.delete_device(device)
      assert Devices.get_device(device.id) == nil
    end

    test "count_organization_devices/1 returns correct count", %{organization: organization, site: site} do
      {:ok, _device1} =
        Devices.create_device(%{
          name: "Router 1",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, _device2} =
        Devices.create_device(%{
          name: "Router 2",
          ip_address: "192.168.1.2",
          site_id: site.id,
          organization_id: organization.id
        })

      assert Devices.count_organization_devices(organization.id) == 2
    end

    test "count_site_devices/1 returns correct count", %{organization: organization, site: site} do
      {:ok, _device1} =
        Devices.create_device(%{
          name: "Router 1",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, _device2} =
        Devices.create_device(%{
          name: "Router 2",
          ip_address: "192.168.1.2",
          site_id: site.id,
          organization_id: organization.id
        })

      assert Devices.count_site_devices(site.id) == 2
    end

    test "count_site_devices_down/1 returns correct count", %{organization: organization, site: site} do
      {:ok, device1} =
        Devices.create_device(%{
          name: "Router 1",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, _device2} =
        Devices.create_device(%{
          name: "Router 2",
          ip_address: "192.168.1.2",
          site_id: site.id,
          organization_id: organization.id
        })

      # Mark one as down
      {:ok, _} = Devices.update_device_status(device1, :down)

      assert Devices.count_site_devices_down(site.id) == 1
    end

    test "list_organization_devices/2 filters by site_id", %{organization: organization, site: site} do
      {:ok, site2} =
        Towerops.Sites.create_site(%{
          name: "Site 2",
          organization_id: organization.id
        })

      {:ok, device1} =
        Devices.create_device(%{
          name: "Router 1",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, _device2} =
        Devices.create_device(%{
          name: "Router 2",
          ip_address: "192.168.1.2",
          site_id: site2.id,
          organization_id: organization.id
        })

      result = Devices.list_organization_devices(organization.id, %{"site_id" => site.id})
      assert length(result) == 1
      assert hd(result).id == device1.id
    end

    test "list_organization_devices/2 filters by status", %{organization: organization, site: site} do
      {:ok, device1} =
        Devices.create_device(%{
          name: "Router 1",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      {:ok, _device2} =
        Devices.create_device(%{
          name: "Router 2",
          ip_address: "192.168.1.2",
          site_id: site.id,
          organization_id: organization.id
        })

      # Mark one as up
      Devices.update_device_status(device1, :up)

      result = Devices.list_organization_devices(organization.id, %{"status" => "up"})
      assert length(result) == 1
      assert hd(result).id == device1.id
    end

    test "get_snmp_config/1 with device ID string", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          snmp_community: "test-community"
        })

      config = Devices.get_snmp_config(device.id)
      assert config.community == "test-community"
      assert config.source == :device
    end
  end

  describe "automatic rediscovery on device updates" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site, user: user}
    end

    test "triggers discovery when snmp_enabled changes from false to true", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: false
        })

      # Clear any existing jobs
      Towerops.Repo.delete_all(Oban.Job)

      # Update to enable SNMP
      {:ok, _updated} = Devices.update_device(device, %{snmp_enabled: true})

      # Check that a discovery job was enqueued
      jobs =
        Oban.Job
        |> Towerops.Repo.all()
        |> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))

      assert [job] = jobs
      assert job.args["device_id"] == device.id
    end

    test "triggers discovery when snmp_version changes while enabled", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          snmp_version: "2c"
        })

      # Clear any existing jobs
      Towerops.Repo.delete_all(Oban.Job)

      # Update SNMP version
      {:ok, _updated} = Devices.update_device(device, %{snmp_version: "1"})

      # Check that a discovery job was enqueued
      jobs =
        Oban.Job
        |> Towerops.Repo.all()
        |> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))

      assert [job] = jobs
      assert job.args["device_id"] == device.id
    end

    test "triggers discovery when snmp_port changes while enabled", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          snmp_port: 161
        })

      # Clear any existing jobs
      Towerops.Repo.delete_all(Oban.Job)

      # Update SNMP port
      {:ok, _updated} = Devices.update_device(device, %{snmp_port: 1161})

      # Check that a discovery job was enqueued
      jobs =
        Oban.Job
        |> Towerops.Repo.all()
        |> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))

      assert [job] = jobs
      assert job.args["device_id"] == device.id
    end

    test "triggers discovery when monitoring_enabled changes to true with SNMP enabled", %{
      organization: organization,
      site: site
    } do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          monitoring_enabled: false
        })

      # Clear any existing jobs
      Towerops.Repo.delete_all(Oban.Job)

      # Enable monitoring
      {:ok, _updated} = Devices.update_device(device, %{monitoring_enabled: true})

      # Check that a discovery job was enqueued
      jobs =
        Oban.Job
        |> Towerops.Repo.all()
        |> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))

      assert [job] = jobs
      assert job.args["device_id"] == device.id
    end

    test "does not trigger discovery when monitoring_enabled changes but SNMP disabled", %{
      organization: organization,
      site: site
    } do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: false,
          monitoring_enabled: false
        })

      # Clear any existing jobs
      Towerops.Repo.delete_all(Oban.Job)

      # Enable monitoring (but SNMP is disabled)
      {:ok, _updated} = Devices.update_device(device, %{monitoring_enabled: true})

      # Check that NO discovery job was enqueued
      jobs =
        Oban.Job
        |> Towerops.Repo.all()
        |> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))

      assert [] = jobs
    end

    test "does not trigger discovery when SNMP is disabled", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true,
          snmp_version: "2c"
        })

      # Clear any existing jobs
      Towerops.Repo.delete_all(Oban.Job)

      # Disable SNMP
      {:ok, _updated} = Devices.update_device(device, %{snmp_enabled: false})

      # Check that NO discovery job was enqueued (SNMP is disabled)
      jobs =
        Oban.Job
        |> Towerops.Repo.all()
        |> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))

      assert [] = jobs
    end

    test "does not trigger discovery for unrelated field changes", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: true
        })

      # Clear any existing jobs
      Towerops.Repo.delete_all(Oban.Job)

      # Update unrelated fields (name, description)
      {:ok, _updated} = Devices.update_device(device, %{name: "Updated Router"})

      # Check that NO discovery job was enqueued
      jobs =
        Oban.Job
        |> Towerops.Repo.all()
        |> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))

      assert [] = jobs
    end

    test "does not trigger discovery when snmp_version changes while disabled", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_enabled: false,
          snmp_version: "2c"
        })

      # Clear any existing jobs
      Towerops.Repo.delete_all(Oban.Job)

      # Update SNMP version (but SNMP is disabled)
      {:ok, _updated} = Devices.update_device(device, %{snmp_version: "1"})

      # Check that NO discovery job was enqueued (SNMP is disabled)
      jobs =
        Oban.Job
        |> Towerops.Repo.all()
        |> Enum.filter(&(&1.worker == "Towerops.Workers.DiscoveryWorker"))

      assert [] = jobs
    end
  end

  describe "property-based tests" do
    import Towerops.AccountsFixtures

    property "creating devices with valid IPs always succeeds" do
      # Create fixtures once outside the property check
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      check all(
              name <- string(:alphanumeric, min_length: 2, max_length: 20),
              octet1 <- integer(0..255),
              octet2 <- integer(0..255),
              octet3 <- integer(0..255),
              octet4 <- integer(1..254),
              max_runs: 20
            ) do
        ip_address = "#{octet1}.#{octet2}.#{octet3}.#{octet4}"

        attrs = %{
          name: name,
          ip_address: ip_address,
          site_id: site.id,
          organization_id: organization.id
        }

        # Use bypass_limits since property tests may create > 10 devices
        assert {:ok, %Device{} = device} = Devices.create_device(attrs, bypass_limits: true)
        assert device.ip_address == ip_address
        assert device.status == :unknown
      end
    end

    property "device status changes are idempotent" do
      check all(status <- member_of([:up, :down, :unknown]), max_runs: 10) do
        user = user_fixture()
        {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

        {:ok, site} =
          Towerops.Sites.create_site(%{
            name: "Test Site",
            organization_id: organization.id
          })

        {:ok, device} =
          Devices.create_device(%{
            name: "Test Device",
            ip_address: "192.168.1.100",
            site_id: site.id,
            organization_id: organization.id
          })

        # Update status twice
        {:ok, updated1} = Devices.update_device_status(device, status)
        {:ok, updated2} = Devices.update_device_status(updated1, status)

        assert updated2.status == status
        # Second update should not change last_status_change_at
        assert updated2.last_status_change_at == updated1.last_status_change_at
      end
    end

    property "device counters are always non-negative" do
      check all(device_count <- integer(0..5), max_runs: 10) do
        user = user_fixture()
        {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

        {:ok, site} =
          Towerops.Sites.create_site(%{
            name: "Test Site",
            organization_id: organization.id
          })

        # Create specified number of devices
        if device_count > 0 do
          for i <- 1..device_count do
            Devices.create_device(%{
              name: "Device #{i}",
              ip_address: "192.168.1.#{i}",
              site_id: site.id,
              organization_id: organization.id
            })
          end
        end

        org_count = Devices.count_organization_devices(organization.id)
        site_count = Devices.count_site_devices(site.id)
        down_count = Devices.count_site_devices_down(site.id)

        assert org_count >= 0
        assert site_count >= 0
        assert down_count >= 0
        assert down_count <= site_count
        assert org_count == site_count
      end
    end

    property "SNMP config prioritization is consistent" do
      check all(
              device_community <- one_of([constant(nil), string(:alphanumeric, min_length: 1, max_length: 20)]),
              site_community <- one_of([constant(nil), string(:alphanumeric, min_length: 1, max_length: 20)]),
              max_runs: 10
            ) do
        user = user_fixture()
        {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

        {:ok, site} =
          Towerops.Sites.create_site(%{
            name: "Test Site",
            organization_id: organization.id
          })

        # Update site with community if provided
        site =
          if site_community do
            {:ok, updated_site} = Towerops.Sites.update_site(site, %{snmp_community: site_community})
            updated_site
          else
            site
          end

        {:ok, device} =
          Devices.create_device(%{
            name: "Router",
            ip_address: "192.168.1.1",
            site_id: site.id,
            organization_id: organization.id,
            snmp_enabled: true,
            snmp_community: device_community
          })

        device = Towerops.Repo.preload(device, site: :organization)
        config = Devices.get_snmp_config(device)

        # Device-level always takes precedence if set
        if device_community do
          assert config.community == device_community
          assert config.source == :device
        else
          if site_community do
            assert config.community == site_community
            assert config.source == :site
          else
            assert config.source in [:organization, :default]
          end
        end
      end
    end
  end

  describe "device creation with subscription limits" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site, user: user}
    end

    test "blocks device creation when at 10-device limit", %{organization: organization, site: site} do
      # Create 10 devices (the limit)
      for i <- 1..10 do
        {:ok, _device} =
          Devices.create_device(%{
            name: "Device #{i}",
            ip_address: "192.168.1.#{i}",
            site_id: site.id,
            organization_id: organization.id,
            monitoring_enabled: false,
            snmp_enabled: false
          })
      end

      # 11th device should be blocked
      assert {:error, changeset} =
               Devices.create_device(%{
                 name: "Device 11",
                 ip_address: "192.168.1.11",
                 site_id: site.id,
                 organization_id: organization.id,
                 monitoring_enabled: false,
                 snmp_enabled: false
               })

      assert %{base: ["You've reached your plan limit of 10 devices. Upgrade to add more."]} =
               errors_on(changeset)
    end

    test "allows device creation when under limit", %{organization: organization, site: site} do
      # Create 9 devices
      for i <- 1..9 do
        {:ok, _device} =
          Devices.create_device(%{
            name: "Device #{i}",
            ip_address: "192.168.1.#{i}",
            site_id: site.id,
            organization_id: organization.id,
            monitoring_enabled: false,
            snmp_enabled: false
          })
      end

      # 10th device should succeed
      assert {:ok, _device} =
               Devices.create_device(%{
                 name: "Device 10",
                 ip_address: "192.168.1.10",
                 site_id: site.id,
                 organization_id: organization.id,
                 monitoring_enabled: false,
                 snmp_enabled: false
               })
    end

    test "bypasses limit with bypass_limits option", %{organization: organization, site: site} do
      # Create 10 devices (at limit)
      for i <- 1..10 do
        {:ok, _device} =
          Devices.create_device(%{
            name: "Device #{i}",
            ip_address: "192.168.1.#{i}",
            site_id: site.id,
            organization_id: organization.id,
            monitoring_enabled: false,
            snmp_enabled: false
          })
      end

      # 11th device succeeds with bypass
      assert {:ok, _device} =
               Devices.create_device(
                 %{
                   name: "Device 11",
                   ip_address: "192.168.1.11",
                   site_id: site.id,
                   organization_id: organization.id,
                   monitoring_enabled: false,
                   snmp_enabled: false
                 },
                 bypass_limits: true
               )
    end

    test "counts devices across multiple sites in same organization", %{
      organization: organization,
      site: site1
    } do
      {:ok, site2} =
        Towerops.Sites.create_site(%{
          name: "Site 2",
          organization_id: organization.id
        })

      # Create 6 devices in site1
      for i <- 1..6 do
        {:ok, _device} =
          Devices.create_device(%{
            name: "Site1 Device #{i}",
            ip_address: "192.168.1.#{i}",
            site_id: site1.id,
            organization_id: organization.id,
            monitoring_enabled: false,
            snmp_enabled: false
          })
      end

      # Create 4 devices in site2 (total = 10)
      for i <- 1..4 do
        {:ok, _device} =
          Devices.create_device(%{
            name: "Site2 Device #{i}",
            ip_address: "192.168.2.#{i}",
            site_id: site2.id,
            organization_id: organization.id,
            monitoring_enabled: false,
            snmp_enabled: false
          })
      end

      # 11th device in either site should be blocked
      assert {:error, changeset} =
               Devices.create_device(%{
                 name: "Site1 Device 7",
                 ip_address: "192.168.1.7",
                 site_id: site1.id,
                 organization_id: organization.id,
                 monitoring_enabled: false,
                 snmp_enabled: false
               })

      assert %{base: ["You've reached your plan limit of 10 devices. Upgrade to add more."]} =
               errors_on(changeset)
    end
  end

  describe "GDPR data access - list_devices_for_organizations/1" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, org1} = Towerops.Organizations.create_organization(%{name: "Org 1"}, user.id)

      # Create org2 with a different owner to avoid subscription limits
      other_user = user_fixture()
      {:ok, org2} = Towerops.Organizations.create_organization(%{name: "Org 2"}, other_user.id)

      {:ok, site1} =
        Towerops.Sites.create_site(%{
          name: "Org1 Site",
          organization_id: org1.id
        })

      {:ok, site2} =
        Towerops.Sites.create_site(%{
          name: "Org2 Site",
          organization_id: org2.id
        })

      %{org1: org1, org2: org2, site1: site1, site2: site2}
    end

    test "returns all devices for given organizations", %{
      org1: org1,
      org2: org2,
      site1: site1,
      site2: site2
    } do
      {:ok, device1} =
        Devices.create_device(%{
          name: "Device 1",
          ip_address: "192.168.1.1",
          site_id: site1.id,
          organization_id: org1.id
        })

      {:ok, device2} =
        Devices.create_device(%{
          name: "Device 2",
          ip_address: "192.168.1.2",
          site_id: site2.id,
          organization_id: org2.id
        })

      result = Devices.list_devices_for_organizations([org1.id, org2.id])
      assert length(result) == 2

      device_ids = Enum.map(result, & &1.id)
      assert device1.id in device_ids
      assert device2.id in device_ids
    end

    test "returns only devices from specified organizations", %{
      org1: org1,
      org2: org2,
      site1: site1,
      site2: site2
    } do
      {:ok, device1} =
        Devices.create_device(%{
          name: "Device 1",
          ip_address: "192.168.1.1",
          site_id: site1.id,
          organization_id: org1.id
        })

      {:ok, _device2} =
        Devices.create_device(%{
          name: "Device 2",
          ip_address: "192.168.1.2",
          site_id: site2.id,
          organization_id: org2.id
        })

      # Only request devices from org1
      result = Devices.list_devices_for_organizations([org1.id])
      assert length(result) == 1
      assert hd(result).id == device1.id
    end

    test "returns empty list when organizations have no devices", %{org1: org1} do
      result = Devices.list_devices_for_organizations([org1.id])
      assert result == []
    end

    test "returns empty list for empty organization list" do
      result = Devices.list_devices_for_organizations([])
      assert result == []
    end

    test "preloads site association", %{org1: org1, site1: site1} do
      {:ok, _device} =
        Devices.create_device(%{
          name: "Device 1",
          ip_address: "192.168.1.1",
          site_id: site1.id,
          organization_id: org1.id
        })

      result = Devices.list_devices_for_organizations([org1.id])
      assert length(result) == 1
      loaded_device = hd(result)
      assert Ecto.assoc_loaded?(loaded_device.site)
      assert loaded_device.site.id == site1.id
    end

    test "orders devices by organization and name", %{org1: org1, org2: org2, site1: site1, site2: site2} do
      {:ok, _zebra_dev} =
        Devices.create_device(%{
          name: "Zebra Device",
          ip_address: "192.168.1.1",
          site_id: site1.id,
          organization_id: org1.id
        })

      {:ok, _alpha_dev} =
        Devices.create_device(%{
          name: "Alpha Device",
          ip_address: "192.168.1.2",
          site_id: site1.id,
          organization_id: org1.id
        })

      {:ok, _beta_dev} =
        Devices.create_device(%{
          name: "Beta Device",
          ip_address: "192.168.2.1",
          site_id: site2.id,
          organization_id: org2.id
        })

      result = Devices.list_devices_for_organizations([org1.id, org2.id])
      assert length(result) == 3

      # Devices should be ordered by org ID, then by name alphabetically
      # Group by org to verify ordering within each org
      org1_devices = Enum.filter(result, &(&1.site.organization_id == org1.id))
      org2_devices = Enum.filter(result, &(&1.site.organization_id == org2.id))

      # Within org1, should be alphabetical by name
      assert length(org1_devices) == 2
      org1_names = Enum.map(org1_devices, & &1.name)
      assert org1_names == ["Alpha Device", "Zebra Device"]

      # Org2 has one device
      assert length(org2_devices) == 1
      assert hd(org2_devices).name == "Beta Device"

      # Overall list should be ordered by org_id first
      org_ids = Enum.map(result, & &1.site.organization_id)
      assert org_ids == Enum.sort(org_ids)
    end
  end

  describe "PubSub broadcasts" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site, user: user}
    end

    test "create_device/1 broadcasts :device_created", %{organization: organization, site: site} do
      Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:org:#{organization.id}")

      {:ok, _device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      assert_receive {:device_created, org_id}
      assert org_id == organization.id
    end

    test "update_device/2 broadcasts :device_updated", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:org:#{organization.id}")

      {:ok, _updated} = Devices.update_device(device, %{name: "Updated Router"})

      assert_receive {:device_updated, org_id}
      assert org_id == organization.id
    end

    test "delete_device/1 broadcasts :device_deleted", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:org:#{organization.id}")

      {:ok, _deleted} = Devices.delete_device(device)

      assert_receive {:device_deleted, org_id}
      assert org_id == organization.id
    end

    test "update_device_status/2 broadcasts :device_status_changed on actual status change", %{
      organization: organization,
      site: site
    } do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:org:#{organization.id}")

      # Status changes from :unknown to :up — should broadcast
      {:ok, _updated} = Devices.update_device_status(device, :up)

      assert_receive {:device_status_changed, org_id}
      assert org_id == organization.id
    end

    test "update_device_status/2 broadcasts :device_updated when status unchanged", %{
      organization: organization,
      site: site
    } do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      # Set initial status to :up
      {:ok, device} = Devices.update_device_status(device, :up)

      Phoenix.PubSub.subscribe(Towerops.PubSub, "devices:org:#{organization.id}")

      # Same status — should broadcast :device_updated (not :device_status_changed)
      {:ok, _updated} = Devices.update_device_status(device, :up)

      refute_receive {:device_status_changed, _}
      assert_receive {:device_updated, org_id}
      assert org_id == organization.id
    end
  end

  describe "get_device_by_ip/3" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site}
    end

    test "returns device matching IP and site", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      found = Devices.get_device_by_ip("192.168.1.1", site.id)
      assert found.id == device.id
    end

    test "returns nil when IP not found in site", %{site: site} do
      assert Devices.get_device_by_ip("10.0.0.1", site.id) == nil
    end

    test "returns nil when site_id is nil" do
      assert Devices.get_device_by_ip("192.168.1.1", nil) == nil
    end

    test "excludes device with given exclude_id", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      assert Devices.get_device_by_ip("192.168.1.1", site.id, device.id) == nil
    end
  end

  describe "get_mikrotik_config/1" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site}
    end

    test "returns mikrotik config from device", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "MikroTik Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          mikrotik_enabled: true,
          mikrotik_username: "admin",
          mikrotik_port: 8729,
          mikrotik_use_ssl: true
        })

      config = Devices.get_mikrotik_config(device)
      assert config.username == "admin"
      assert config.port == 8729
      assert config.use_ssl == true
      assert config.enabled == true
    end

    test "returns defaults when fields are nil", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      config = Devices.get_mikrotik_config(device)
      assert config.port == 8729
      assert config.ssh_port == 22
      assert config.use_ssl == true
      assert config.enabled == false
    end

    test "accepts device_id string", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          mikrotik_enabled: true,
          mikrotik_username: "admin"
        })

      config = Devices.get_mikrotik_config(device.id)
      assert config.username == "admin"
    end
  end

  describe "get_snmpv3_config/1" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site}
    end

    test "returns nil for non-v3 device", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_version: "2c"
        })

      assert Devices.get_snmpv3_config(device) == nil
    end

    test "returns v3 config for v3 device", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_version: "3",
          snmpv3_security_level: "authPriv",
          snmpv3_username: "snmpuser",
          snmpv3_auth_protocol: "SHA-256",
          snmpv3_auth_password: "authpass123",
          snmpv3_priv_protocol: "AES",
          snmpv3_priv_password: "privpass123"
        })

      config = Devices.get_snmpv3_config(device)
      assert config.security_level == "authPriv"
      assert config.username == "snmpuser"
      assert config.auth_protocol == "SHA-256"
      assert config.priv_protocol == "AES"
    end

    test "accepts device_id string", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_version: "3",
          snmpv3_username: "snmpuser",
          snmpv3_auth_password: "authpass123",
          snmpv3_priv_password: "privpass123"
        })

      config = Devices.get_snmpv3_config(device.id)
      assert config.username == "snmpuser"
    end
  end

  describe "propagate_site_community_change/2" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      # Set community on site so devices will inherit from it
      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id,
          snmp_community: "site-community"
        })

      %{organization: organization, site: site}
    end

    test "updates devices inheriting from site", %{organization: organization, site: site} do
      # Device created without community → inherits from site, source = "site"
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_community: nil
        })

      assert device.snmp_community_source == "site"

      assert :ok = Devices.propagate_site_community_change(site.id, "new-community")

      updated = Devices.get_device!(device.id)
      assert updated.snmp_community == "new-community"
    end

    test "does not update devices with device-level community", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_community: "device-community"
        })

      assert device.snmp_community_source == "device"

      assert :ok = Devices.propagate_site_community_change(site.id, "new-community")

      updated = Devices.get_device!(device.id)
      assert updated.snmp_community == "device-community"
    end
  end

  describe "propagate_organization_community_change/2" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()

      {:ok, organization} =
        Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      # Update org with community so devices can inherit from it
      {:ok, organization} =
        Towerops.Organizations.update_organization(organization, %{snmp_community: "org-community"})

      # Site without community so devices inherit from org
      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site}
    end

    test "updates devices inheriting from organization", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_community: nil
        })

      assert device.snmp_community_source == "organization"

      assert :ok = Devices.propagate_organization_community_change(organization.id, "new-community")

      updated = Devices.get_device!(device.id)
      assert updated.snmp_community == "new-community"
    end

    test "does not update devices with device-level source", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_community: "device-community"
        })

      assert device.snmp_community_source == "device"

      assert :ok = Devices.propagate_organization_community_change(organization.id, "new-community")

      updated = Devices.get_device!(device.id)
      assert updated.snmp_community == "device-community"
    end
  end

  describe "propagate_site_mikrotik_change/2" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      # Set mikrotik creds on site so devices inherit
      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id,
          mikrotik_username: "site-user"
        })

      %{organization: organization, site: site}
    end

    test "updates devices inheriting mikrotik from site", %{organization: organization, site: site} do
      # Device without mikrotik_username → inherits from site
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      assert device.mikrotik_credential_source == "site"

      assert :ok =
               Devices.propagate_site_mikrotik_change(site.id, %{
                 mikrotik_username: "new-user"
               })

      updated = Devices.get_device!(device.id)
      assert updated.mikrotik_username == "new-user"
    end
  end

  describe "propagate_organization_mikrotik_change/2" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      # Set mikrotik creds on org so devices inherit
      {:ok, organization} =
        Towerops.Organizations.update_organization(organization, %{mikrotik_username: "org-user"})

      # Site without mikrotik creds so devices inherit from org
      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site}
    end

    test "updates devices inheriting mikrotik from organization", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id
        })

      assert device.mikrotik_credential_source == "organization"

      assert :ok =
               Devices.propagate_organization_mikrotik_change(organization.id, %{
                 mikrotik_username: "new-user"
               })

      updated = Devices.get_device!(device.id)
      assert updated.mikrotik_username == "new-user"
    end
  end

  describe "propagate_site_snmpv3_change/2" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      # Set snmpv3 creds on site
      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id,
          snmpv3_username: "site-v3-user",
          snmpv3_auth_password: "authpass123",
          snmpv3_priv_password: "privpass123"
        })

      %{organization: organization, site: site}
    end

    test "updates devices inheriting snmpv3 from site", %{organization: organization, site: site} do
      # Create device and manually set snmpv3_credential_source to "site"
      # (can't use create_device with v3 + no username due to changeset validation order)
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmpv3_username: "site-v3-user",
          snmpv3_auth_password: "authpass123",
          snmpv3_priv_password: "privpass123",
          snmp_version: "3"
        })

      # Force source to "site" for propagation test
      device
      |> Ecto.Changeset.change(%{snmpv3_credential_source: "site"})
      |> Towerops.Repo.update!()

      assert :ok =
               Devices.propagate_site_snmpv3_change(site.id, %{
                 snmpv3_username: "new-v3-user"
               })

      updated = Devices.get_device!(device.id)
      assert updated.snmpv3_username == "new-v3-user"
    end
  end

  describe "propagate_organization_snmpv3_change/2" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      # Set snmpv3 creds on org (with passwords required for v3 validation)
      {:ok, organization} =
        Towerops.Organizations.update_organization(organization, %{
          snmpv3_username: "org-v3-user",
          snmpv3_auth_password: "authpass123",
          snmpv3_priv_password: "privpass123"
        })

      # Site without snmpv3 creds
      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site}
    end

    test "updates devices inheriting snmpv3 from organization", %{organization: organization, site: site} do
      # Create device with v3 credentials and force source to "organization"
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmpv3_username: "org-v3-user",
          snmpv3_auth_password: "authpass123",
          snmpv3_priv_password: "privpass123",
          snmp_version: "3"
        })

      # Force source to "organization" for propagation test
      device
      |> Ecto.Changeset.change(%{snmpv3_credential_source: "organization"})
      |> Towerops.Repo.update!()

      assert :ok =
               Devices.propagate_organization_snmpv3_change(organization.id, %{
                 snmpv3_username: "new-v3-user"
               })

      updated = Devices.get_device!(device.id)
      assert updated.snmpv3_username == "new-v3-user"
    end
  end

  describe "resolve_snmp_community/1" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site}
    end

    test "returns device community when source is device", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_community: "my-community"
        })

      assert Devices.resolve_snmp_community(device) == "my-community"
    end

    test "returns site community when device has none", %{organization: organization, site: site} do
      {:ok, site} = Towerops.Sites.update_site(site, %{snmp_community: "site-community"})

      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_community: nil,
          snmp_community_source: "site"
        })

      assert Devices.resolve_snmp_community(device) == "site-community"
    end

    test "returns org community when device and site have none", %{organization: organization, site: site} do
      {:ok, _org} =
        Towerops.Organizations.update_organization(organization, %{snmp_community: "org-community"})

      {:ok, device} =
        Devices.create_device(%{
          name: "Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          snmp_community: nil,
          snmp_community_source: "organization"
        })

      assert Devices.resolve_snmp_community(device) == "org-community"
    end
  end

  describe "list_mikrotik_devices_with_api/0" do
    import Towerops.AccountsFixtures

    setup do
      user = user_fixture()
      {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)

      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Test Site",
          organization_id: organization.id
        })

      %{organization: organization, site: site}
    end

    test "returns devices with mikrotik enabled and username", %{organization: organization, site: site} do
      {:ok, device} =
        Devices.create_device(%{
          name: "MikroTik Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          mikrotik_enabled: true,
          mikrotik_username: "admin"
        })

      result = Devices.list_mikrotik_devices_with_api()
      device_ids = Enum.map(result, & &1.id)
      assert device.id in device_ids
    end

    test "excludes devices without mikrotik enabled", %{organization: organization, site: site} do
      {:ok, _device} =
        Devices.create_device(%{
          name: "Regular Router",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          mikrotik_enabled: false
        })

      result = Devices.list_mikrotik_devices_with_api()
      assert result == []
    end
  end
end
