defmodule Towerops.SnmpTest do
  use Towerops.DataCase

  import Towerops.AccountsFixtures

  alias Towerops.Snmp
  alias Towerops.Snmp.ArpEntry
  alias Towerops.Snmp.Device
  alias Towerops.Snmp.Interface
  alias Towerops.Snmp.InterfaceStat
  alias Towerops.Snmp.MacAddress
  alias Towerops.Snmp.Neighbor
  alias Towerops.Snmp.Processor
  alias Towerops.Snmp.Sensor
  alias Towerops.Snmp.SensorReading
  alias Towerops.Snmp.SnmpMock
  alias Towerops.Snmp.Storage
  alias Towerops.Snmp.StorageReading

  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} =
      Towerops.Devices.create_device(%{
        name: "Test Router",
        ip_address: "192.168.1.1",
        snmp_enabled: true,
        snmp_version: "2c",
        snmp_community: "public",
        snmp_port: 161,
        site_id: site.id,
        organization_id: organization.id
      })

    snmp_device =
      %Device{}
      |> Device.changeset(%{
        device_id: device.id,
        sys_name: "test-router",
        sys_descr: "Test Device"
      })
      |> Repo.insert!()

    %{device: device, snmp_device: snmp_device, organization: organization}
  end

  describe "test_connection/4" do
    import Mox

    setup :verify_on_exit!

    test "delegates to Client.test_connection" do
      expect(SnmpMock, :get, fn _, _, _ ->
        {:ok, 12_345}
      end)

      result = Snmp.test_connection("192.168.1.1", "public", "2c")

      case result do
        {:ok, _} -> assert true
        {:error, _} -> assert true
      end
    end

    test "accepts custom port" do
      expect(SnmpMock, :get, fn _, _, _ ->
        {:ok, 12_345}
      end)

      result = Snmp.test_connection("192.168.1.1", "public", "2c", 1161)

      case result do
        {:ok, _} -> assert true
        {:error, _} -> assert true
      end
    end
  end

  describe "get_device/1" do
    test "returns device for device_id", %{device: device, snmp_device: snmp_device} do
      found_device = Snmp.get_device(device.id)
      assert found_device.id == snmp_device.id
      assert found_device.sys_name == "test-router"
    end

    test "returns nil for non-existent device_id" do
      assert Snmp.get_device(Ecto.UUID.generate()) == nil
    end
  end

  describe "get_device_with_associations/1" do
    test "returns device with preloaded associations", %{device: device, snmp_device: snmp_device} do
      found_device = Snmp.get_device_with_associations(device.id)
      assert found_device.id == snmp_device.id
      assert Ecto.assoc_loaded?(found_device.interfaces)
      assert Ecto.assoc_loaded?(found_device.sensors)
    end

    test "returns nil for non-existent device_id" do
      assert Snmp.get_device_with_associations(Ecto.UUID.generate()) == nil
    end
  end

  describe "list_interfaces/1" do
    test "returns all interfaces for a device ordered by if_index", %{device: _device, snmp_device: snmp_device} do
      interface1 =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 2,
          if_name: "eth1"
        })
        |> Repo.insert!()

      interface2 =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      interfaces = Snmp.list_interfaces(snmp_device.id)
      assert length(interfaces) == 2
      assert hd(interfaces).id == interface2.id
      assert List.last(interfaces).id == interface1.id
    end

    test "returns empty list for device with no interfaces", %{device: _device, snmp_device: snmp_device} do
      assert Snmp.list_interfaces(snmp_device.id) == []
    end
  end

  describe "list_monitored_interfaces/1" do
    test "returns only monitored interfaces", %{device: _device, snmp_device: snmp_device} do
      monitored =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0",
          monitored: true
        })
        |> Repo.insert!()

      _unmonitored =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 2,
          if_name: "eth1",
          monitored: false
        })
        |> Repo.insert!()

      interfaces = Snmp.list_monitored_interfaces(snmp_device.id)
      assert length(interfaces) == 1
      assert hd(interfaces).id == monitored.id
    end
  end

  describe "get_interface/1" do
    test "returns interface by id", %{device: _device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      found = Snmp.get_interface(interface.id)
      assert found.id == interface.id
      assert found.if_name == "eth0"
    end

    test "returns nil for non-existent id" do
      assert Snmp.get_interface(Ecto.UUID.generate()) == nil
    end
  end

  describe "update_interface/2" do
    test "updates interface attributes", %{device: _device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0",
          monitored: false
        })
        |> Repo.insert!()

      {:ok, updated} = Snmp.update_interface(interface, %{monitored: true})
      assert updated.monitored == true
    end

    test "returns error for invalid attributes", %{device: _device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      {:error, changeset} = Snmp.update_interface(interface, %{if_index: nil})
      assert changeset.errors[:if_index]
    end
  end

  describe "list_sensors/1" do
    test "returns all sensors for a device ordered by type and index", %{device: _device, snmp_device: snmp_device} do
      sensor1 =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      sensor2 =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "power",
          sensor_index: "2",
          sensor_oid: "1.2.3.5"
        })
        |> Repo.insert!()

      sensors = Snmp.list_sensors(snmp_device.id)
      assert length(sensors) == 2
      assert sensor1.id in Enum.map(sensors, & &1.id)
      assert sensor2.id in Enum.map(sensors, & &1.id)
    end

    test "returns empty list for device with no sensors", %{device: _device, snmp_device: snmp_device} do
      assert Snmp.list_sensors(snmp_device.id) == []
    end
  end

  describe "list_monitored_sensors/1" do
    test "returns only monitored sensors", %{device: _device, snmp_device: snmp_device} do
      monitored =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4",
          monitored: true
        })
        |> Repo.insert!()

      _unmonitored =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "power",
          sensor_index: "2",
          sensor_oid: "1.2.3.5",
          monitored: false
        })
        |> Repo.insert!()

      sensors = Snmp.list_monitored_sensors(snmp_device.id)
      assert length(sensors) == 1
      assert hd(sensors).id == monitored.id
    end
  end

  describe "list_sensors_by_type/1" do
    test "groups sensors by type", %{device: _device, snmp_device: snmp_device} do
      _temp1 =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      _temp2 =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "2",
          sensor_oid: "1.2.3.5"
        })
        |> Repo.insert!()

      _power =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "power",
          sensor_index: "3",
          sensor_oid: "1.2.3.6"
        })
        |> Repo.insert!()

      grouped = Snmp.list_sensors_by_type(snmp_device.id)
      assert Map.has_key?(grouped, "temperature")
      assert Map.has_key?(grouped, "power")
      assert length(grouped["temperature"]) == 2
      assert length(grouped["power"]) == 1
    end

    test "returns empty map for device with no sensors", %{device: _device, snmp_device: snmp_device} do
      assert Snmp.list_sensors_by_type(snmp_device.id) == %{}
    end
  end

  describe "get_sensor/1" do
    test "returns sensor by id", %{device: _device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      found = Snmp.get_sensor(sensor.id)
      assert found.id == sensor.id
      assert found.sensor_type == "temperature"
    end

    test "returns nil for non-existent id" do
      assert Snmp.get_sensor(Ecto.UUID.generate()) == nil
    end
  end

  describe "update_sensor/2" do
    test "updates sensor attributes", %{device: _device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4",
          monitored: false
        })
        |> Repo.insert!()

      {:ok, updated} = Snmp.update_sensor(sensor, %{monitored: true})
      assert updated.monitored == true
    end

    test "returns error for invalid attributes", %{device: _device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      {:error, changeset} = Snmp.update_sensor(sensor, %{sensor_oid: nil})
      assert changeset.errors[:sensor_oid]
    end
  end

  describe "get_sensor_readings/2" do
    test "returns recent readings for a sensor", %{device: _device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      for i <- 1..5 do
        %SensorReading{}
        |> SensorReading.changeset(%{
          sensor_id: sensor.id,
          value: 20.0 + i,
          status: "ok",
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()
      end

      readings = Snmp.get_sensor_readings(sensor.id)
      assert length(readings) == 5
    end

    test "respects limit option", %{device: _device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      for i <- 1..10 do
        %SensorReading{}
        |> SensorReading.changeset(%{
          sensor_id: sensor.id,
          value: 20.0 + i,
          status: "ok",
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()
      end

      readings = Snmp.get_sensor_readings(sensor.id, limit: 3)
      assert length(readings) == 3
    end

    test "respects since option", %{device: _device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      past = DateTime.add(DateTime.utc_now(), -3600, :second)
      recent = DateTime.add(DateTime.utc_now(), -60, :second)

      %SensorReading{}
      |> SensorReading.changeset(%{
        sensor_id: sensor.id,
        value: 20.0,
        status: "ok",
        checked_at: past
      })
      |> Repo.insert!()

      %SensorReading{}
      |> SensorReading.changeset(%{
        sensor_id: sensor.id,
        value: 25.0,
        status: "ok",
        checked_at: recent
      })
      |> Repo.insert!()

      since_time = DateTime.add(DateTime.utc_now(), -120, :second)
      readings = Snmp.get_sensor_readings(sensor.id, since: since_time)
      assert length(readings) == 1
      assert hd(readings).value == 25.0
    end

    test "returns empty list for sensor with no readings", %{device: _device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      assert Snmp.get_sensor_readings(sensor.id) == []
    end
  end

  describe "get_latest_sensor_reading/1" do
    test "returns most recent reading", %{device: _device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      old_reading =
        %SensorReading{}
        |> SensorReading.changeset(%{
          sensor_id: sensor.id,
          value: 20.0,
          status: "ok",
          checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
        })
        |> Repo.insert!()

      new_reading =
        %SensorReading{}
        |> SensorReading.changeset(%{
          sensor_id: sensor.id,
          value: 25.0,
          status: "ok",
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      latest = Snmp.get_latest_sensor_reading(sensor.id)
      assert latest.id == new_reading.id
      refute latest.id == old_reading.id
    end

    test "returns nil for sensor with no readings", %{device: _device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      assert Snmp.get_latest_sensor_reading(sensor.id) == nil
    end
  end

  describe "get_interface_stats/2" do
    test "returns recent stats for an interface", %{device: _device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      for i <- 1..5 do
        %InterfaceStat{}
        |> InterfaceStat.changeset(%{
          interface_id: interface.id,
          if_in_octets: i * 1000,
          if_out_octets: i * 2000,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()
      end

      stats = Snmp.get_interface_stats(interface.id)
      assert length(stats) == 5
    end

    test "respects limit option", %{device: _device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      for i <- 1..10 do
        %InterfaceStat{}
        |> InterfaceStat.changeset(%{
          interface_id: interface.id,
          if_in_octets: i * 1000,
          if_out_octets: i * 2000,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()
      end

      stats = Snmp.get_interface_stats(interface.id, limit: 3)
      assert length(stats) == 3
    end

    test "respects since option", %{device: _device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      past = DateTime.add(DateTime.utc_now(), -3600, :second)
      recent = DateTime.add(DateTime.utc_now(), -60, :second)

      %InterfaceStat{}
      |> InterfaceStat.changeset(%{
        interface_id: interface.id,
        if_in_octets: 1000,
        if_out_octets: 2000,
        checked_at: past
      })
      |> Repo.insert!()

      %InterfaceStat{}
      |> InterfaceStat.changeset(%{
        interface_id: interface.id,
        if_in_octets: 5000,
        if_out_octets: 6000,
        checked_at: recent
      })
      |> Repo.insert!()

      since_time = DateTime.add(DateTime.utc_now(), -120, :second)
      stats = Snmp.get_interface_stats(interface.id, since: since_time)
      assert length(stats) == 1
      assert hd(stats).if_in_octets == 5000
    end

    test "returns empty list for interface with no stats", %{device: _device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      assert Snmp.get_interface_stats(interface.id) == []
    end
  end

  describe "get_latest_interface_stat/1" do
    test "returns most recent stat", %{device: _device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      old_stat =
        %InterfaceStat{}
        |> InterfaceStat.changeset(%{
          interface_id: interface.id,
          if_in_octets: 1000,
          if_out_octets: 2000,
          checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
        })
        |> Repo.insert!()

      new_stat =
        %InterfaceStat{}
        |> InterfaceStat.changeset(%{
          interface_id: interface.id,
          if_in_octets: 5000,
          if_out_octets: 6000,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      latest = Snmp.get_latest_interface_stat(interface.id)
      assert latest.id == new_stat.id
      refute latest.id == old_stat.id
    end

    test "returns nil for interface with no stats", %{device: _device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      assert Snmp.get_latest_interface_stat(interface.id) == nil
    end
  end

  describe "create_sensor_reading/1" do
    test "creates a sensor reading with valid attributes", %{device: _device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      {:ok, reading} =
        Snmp.create_sensor_reading(%{
          sensor_id: sensor.id,
          value: 25.5,
          status: "ok",
          checked_at: DateTime.utc_now()
        })

      assert reading.sensor_id == sensor.id
      assert reading.value == 25.5
      assert reading.status == "ok"
    end

    test "returns error with invalid attributes" do
      {:error, changeset} = Snmp.create_sensor_reading(%{value: "not a number"})
      assert changeset.errors[:sensor_id]
    end
  end

  describe "create_interface_stat/1" do
    test "creates an interface stat with valid attributes", %{device: _device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      {:ok, stat} =
        Snmp.create_interface_stat(%{
          interface_id: interface.id,
          if_in_octets: 1000,
          if_out_octets: 2000,
          if_in_errors: 0,
          if_out_errors: 0,
          if_in_discards: 0,
          if_out_discards: 0,
          checked_at: DateTime.utc_now()
        })

      assert stat.interface_id == interface.id
      assert stat.if_in_octets == 1000
      assert stat.if_out_octets == 2000
    end

    test "returns error with invalid attributes" do
      {:error, changeset} = Snmp.create_interface_stat(%{if_in_octets: "not a number"})
      assert changeset.errors[:interface_id]
    end
  end

  describe "get_latest_sensor_readings_batch/1" do
    test "returns empty map for empty sensor list", %{device: _device, snmp_device: _snmp_device} do
      assert Snmp.get_latest_sensor_readings_batch([]) == %{}
    end

    test "returns readings for multiple sensors", %{device: _device, snmp_device: snmp_device} do
      sensor1 =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      sensor2 =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "power",
          sensor_index: "2",
          sensor_oid: "1.2.3.5"
        })
        |> Repo.insert!()

      reading1 =
        %SensorReading{}
        |> SensorReading.changeset(%{
          sensor_id: sensor1.id,
          value: 25.0,
          status: "ok",
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      reading2 =
        %SensorReading{}
        |> SensorReading.changeset(%{
          sensor_id: sensor2.id,
          value: 100.0,
          status: "ok",
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      batch = Snmp.get_latest_sensor_readings_batch([sensor1.id, sensor2.id])
      assert map_size(batch) == 2
      assert batch[sensor1.id].id == reading1.id
      assert batch[sensor2.id].id == reading2.id
    end

    test "returns only latest reading per sensor", %{device: _device, snmp_device: snmp_device} do
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      _old_reading =
        %SensorReading{}
        |> SensorReading.changeset(%{
          sensor_id: sensor.id,
          value: 20.0,
          status: "ok",
          checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
        })
        |> Repo.insert!()

      new_reading =
        %SensorReading{}
        |> SensorReading.changeset(%{
          sensor_id: sensor.id,
          value: 25.0,
          status: "ok",
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      batch = Snmp.get_latest_sensor_readings_batch([sensor.id])
      assert map_size(batch) == 1
      assert batch[sensor.id].id == new_reading.id
      assert batch[sensor.id].value == 25.0
    end

    test "excludes sensors without readings", %{device: _device, snmp_device: snmp_device} do
      sensor_with_reading =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: "1.2.3.4"
        })
        |> Repo.insert!()

      sensor_without_reading =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "power",
          sensor_index: "2",
          sensor_oid: "1.2.3.5"
        })
        |> Repo.insert!()

      %SensorReading{}
      |> SensorReading.changeset(%{
        sensor_id: sensor_with_reading.id,
        value: 25.0,
        status: "ok",
        checked_at: DateTime.utc_now()
      })
      |> Repo.insert!()

      batch = Snmp.get_latest_sensor_readings_batch([sensor_with_reading.id, sensor_without_reading.id])
      assert map_size(batch) == 1
      assert Map.has_key?(batch, sensor_with_reading.id)
      refute Map.has_key?(batch, sensor_without_reading.id)
    end
  end

  describe "get_latest_interface_stats_batch/1" do
    test "returns empty map for empty interface list", %{device: _device, snmp_device: _snmp_device} do
      assert Snmp.get_latest_interface_stats_batch([]) == %{}
    end

    test "returns stats for multiple interfaces", %{device: _device, snmp_device: snmp_device} do
      interface1 =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      interface2 =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 2,
          if_name: "eth1"
        })
        |> Repo.insert!()

      stat1 =
        %InterfaceStat{}
        |> InterfaceStat.changeset(%{
          interface_id: interface1.id,
          if_in_octets: 1000,
          if_out_octets: 2000,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      stat2 =
        %InterfaceStat{}
        |> InterfaceStat.changeset(%{
          interface_id: interface2.id,
          if_in_octets: 5000,
          if_out_octets: 6000,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      batch = Snmp.get_latest_interface_stats_batch([interface1.id, interface2.id])
      assert map_size(batch) == 2
      assert batch[interface1.id].id == stat1.id
      assert batch[interface2.id].id == stat2.id
    end

    test "returns only latest stat per interface", %{device: _device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      _old_stat =
        %InterfaceStat{}
        |> InterfaceStat.changeset(%{
          interface_id: interface.id,
          if_in_octets: 1000,
          if_out_octets: 2000,
          checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
        })
        |> Repo.insert!()

      new_stat =
        %InterfaceStat{}
        |> InterfaceStat.changeset(%{
          interface_id: interface.id,
          if_in_octets: 5000,
          if_out_octets: 6000,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      batch = Snmp.get_latest_interface_stats_batch([interface.id])
      assert map_size(batch) == 1
      assert batch[interface.id].id == new_stat.id
      assert batch[interface.id].if_in_octets == 5000
    end

    test "excludes interfaces without stats", %{device: _device, snmp_device: snmp_device} do
      interface_with_stat =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      interface_without_stat =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 2,
          if_name: "eth1"
        })
        |> Repo.insert!()

      %InterfaceStat{}
      |> InterfaceStat.changeset(%{
        interface_id: interface_with_stat.id,
        if_in_octets: 1000,
        if_out_octets: 2000,
        checked_at: DateTime.utc_now()
      })
      |> Repo.insert!()

      batch = Snmp.get_latest_interface_stats_batch([interface_with_stat.id, interface_without_stat.id])
      assert map_size(batch) == 1
      assert Map.has_key?(batch, interface_with_stat.id)
      refute Map.has_key?(batch, interface_without_stat.id)
    end
  end

  describe "list_neighbors/1" do
    test "returns all neighbors for a device", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      neighbor =
        %Neighbor{}
        |> Neighbor.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          protocol: "lldp",
          remote_chassis_id: "00:11:22:33:44:55",
          remote_system_name: "neighbor-switch",
          last_discovered_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      neighbors = Snmp.list_neighbors(device.id)
      assert length(neighbors) == 1
      assert hd(neighbors).id == neighbor.id
      assert Ecto.assoc_loaded?(hd(neighbors).interface)
    end

    test "returns empty list for device with no neighbors", %{device: device, snmp_device: _snmp_device} do
      assert Snmp.list_neighbors(device.id) == []
    end

    test "orders neighbors by protocol and system name", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      neighbor_lldp_b =
        %Neighbor{}
        |> Neighbor.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          protocol: "lldp",
          remote_chassis_id: "00:11:22:33:44:55",
          remote_system_name: "switch-b",
          last_discovered_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      neighbor_cdp_a =
        %Neighbor{}
        |> Neighbor.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          protocol: "cdp",
          remote_chassis_id: "aa:bb:cc:dd:ee:ff",
          remote_system_name: "router-a",
          last_discovered_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      neighbor_lldp_a =
        %Neighbor{}
        |> Neighbor.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          protocol: "lldp",
          remote_chassis_id: "11:22:33:44:55:66",
          remote_system_name: "switch-a",
          last_discovered_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      neighbors = Snmp.list_neighbors(device.id)
      assert length(neighbors) == 3
      # Should be ordered: cdp first, then lldp alphabetically
      assert Enum.at(neighbors, 0).id == neighbor_cdp_a.id
      assert Enum.at(neighbors, 1).id == neighbor_lldp_a.id
      assert Enum.at(neighbors, 2).id == neighbor_lldp_b.id
    end
  end

  describe "get_neighbor/1" do
    test "returns neighbor by id", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      neighbor =
        %Neighbor{}
        |> Neighbor.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          protocol: "lldp",
          remote_chassis_id: "00:11:22:33:44:55",
          last_discovered_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      found = Snmp.get_neighbor(neighbor.id)
      assert found.id == neighbor.id
      assert found.remote_chassis_id == "00:11:22:33:44:55"
    end

    test "returns nil for non-existent id" do
      assert Snmp.get_neighbor(Ecto.UUID.generate()) == nil
    end
  end

  describe "upsert_neighbor/1" do
    test "creates new neighbor when none exists", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "lldp",
        remote_chassis_id: "00:11:22:33:44:55",
        remote_system_name: "new-neighbor",
        last_discovered_at: DateTime.utc_now()
      }

      {:ok, neighbor} = Snmp.upsert_neighbor(attrs)
      assert neighbor.remote_system_name == "new-neighbor"
      assert neighbor.remote_chassis_id == "00:11:22:33:44:55"
    end

    test "updates existing neighbor when match found", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      existing =
        %Neighbor{}
        |> Neighbor.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          protocol: "lldp",
          remote_chassis_id: "00:11:22:33:44:55",
          remote_system_name: "old-name",
          last_discovered_at: DateTime.add(DateTime.utc_now(), -3600, :second)
        })
        |> Repo.insert!()

      new_time = DateTime.truncate(DateTime.utc_now(), :second)

      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        protocol: "lldp",
        remote_chassis_id: "00:11:22:33:44:55",
        remote_system_name: "updated-name",
        last_discovered_at: new_time
      }

      {:ok, updated} = Snmp.upsert_neighbor(attrs)
      assert updated.id == existing.id
      assert updated.remote_system_name == "updated-name"
      # Compare with truncation since database stores :utc_datetime (no microseconds)
      assert DateTime.compare(updated.last_discovered_at, new_time) in [:eq, :gt]
    end
  end

  describe "delete_stale_neighbors/2" do
    test "deletes neighbors older than cutoff", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      old_time = DateTime.add(DateTime.utc_now(), -7200, :second)
      recent_time = DateTime.add(DateTime.utc_now(), -60, :second)

      old_neighbor =
        %Neighbor{}
        |> Neighbor.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          protocol: "lldp",
          remote_chassis_id: "00:11:22:33:44:55",
          last_discovered_at: old_time
        })
        |> Repo.insert!()

      recent_neighbor =
        %Neighbor{}
        |> Neighbor.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          protocol: "cdp",
          remote_chassis_id: "aa:bb:cc:dd:ee:ff",
          last_discovered_at: recent_time
        })
        |> Repo.insert!()

      cutoff = DateTime.add(DateTime.utc_now(), -3600, :second)
      {deleted_count, _} = Snmp.delete_stale_neighbors(device.id, cutoff)

      assert deleted_count == 1
      assert Snmp.get_neighbor(old_neighbor.id) == nil
      assert Snmp.get_neighbor(recent_neighbor.id)
    end

    test "does not delete neighbors from other devices", %{organization: organization} do
      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Another Site",
          organization_id: organization.id
        })

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

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

      snmp_device1 =
        %Device{}
        |> Device.changeset(%{
          device_id: device1.id,
          sys_name: "router1"
        })
        |> Repo.insert!()

      snmp_device2 =
        %Device{}
        |> Device.changeset(%{
          device_id: device2.id,
          sys_name: "router2"
        })
        |> Repo.insert!()

      interface1 =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device1.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      interface2 =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device2.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      old_time = DateTime.add(DateTime.utc_now(), -7200, :second)

      neighbor1 =
        %Neighbor{}
        |> Neighbor.changeset(%{
          device_id: device1.id,
          interface_id: interface1.id,
          protocol: "lldp",
          last_discovered_at: old_time
        })
        |> Repo.insert!()

      neighbor2 =
        %Neighbor{}
        |> Neighbor.changeset(%{
          device_id: device2.id,
          interface_id: interface2.id,
          protocol: "lldp",
          last_discovered_at: old_time
        })
        |> Repo.insert!()

      cutoff = DateTime.add(DateTime.utc_now(), -3600, :second)
      {deleted_count, _} = Snmp.delete_stale_neighbors(device1.id, cutoff)

      assert deleted_count == 1
      assert Snmp.get_neighbor(neighbor1.id) == nil
      assert Snmp.get_neighbor(neighbor2.id)
    end
  end

  describe "get_storage_readings/2" do
    test "returns recent readings for a storage entry", %{snmp_device: snmp_device} do
      storage =
        %Storage{}
        |> Storage.changeset(%{
          snmp_device_id: snmp_device.id,
          storage_index: 1,
          storage_type: "fixed_disk",
          description: "/dev/sda1"
        })
        |> Repo.insert!()

      for i <- 1..5 do
        %StorageReading{}
        |> StorageReading.changeset(%{
          storage_id: storage.id,
          used_bytes: 1000 * i,
          total_bytes: 10_000,
          usage_percent: 10.0 * i,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()
      end

      readings = Snmp.get_storage_readings(storage.id)
      assert length(readings) == 5
    end

    test "respects limit option", %{snmp_device: snmp_device} do
      storage =
        %Storage{}
        |> Storage.changeset(%{
          snmp_device_id: snmp_device.id,
          storage_index: 1,
          storage_type: "fixed_disk",
          description: "/dev/sda1"
        })
        |> Repo.insert!()

      for i <- 1..10 do
        %StorageReading{}
        |> StorageReading.changeset(%{
          storage_id: storage.id,
          used_bytes: 1000 * i,
          total_bytes: 10_000,
          usage_percent: 10.0 * i,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()
      end

      readings = Snmp.get_storage_readings(storage.id, limit: 3)
      assert length(readings) == 3
    end

    test "respects since option", %{snmp_device: snmp_device} do
      storage =
        %Storage{}
        |> Storage.changeset(%{
          snmp_device_id: snmp_device.id,
          storage_index: 1,
          storage_type: "fixed_disk",
          description: "/dev/sda1"
        })
        |> Repo.insert!()

      past = DateTime.add(DateTime.utc_now(), -3600, :second)
      recent = DateTime.add(DateTime.utc_now(), -60, :second)

      %StorageReading{}
      |> StorageReading.changeset(%{
        storage_id: storage.id,
        used_bytes: 1000,
        total_bytes: 10_000,
        usage_percent: 10.0,
        checked_at: past
      })
      |> Repo.insert!()

      %StorageReading{}
      |> StorageReading.changeset(%{
        storage_id: storage.id,
        used_bytes: 5000,
        total_bytes: 10_000,
        usage_percent: 50.0,
        checked_at: recent
      })
      |> Repo.insert!()

      since_time = DateTime.add(DateTime.utc_now(), -120, :second)
      readings = Snmp.get_storage_readings(storage.id, since: since_time)
      assert length(readings) == 1
      assert hd(readings).usage_percent == 50.0
    end

    test "returns empty list for storage with no readings", %{snmp_device: snmp_device} do
      storage =
        %Storage{}
        |> Storage.changeset(%{
          snmp_device_id: snmp_device.id,
          storage_index: 1,
          storage_type: "fixed_disk",
          description: "/dev/sda1"
        })
        |> Repo.insert!()

      assert Snmp.get_storage_readings(storage.id) == []
    end
  end

  describe "get_latest_storage_reading/1" do
    test "returns most recent reading", %{snmp_device: snmp_device} do
      storage =
        %Storage{}
        |> Storage.changeset(%{
          snmp_device_id: snmp_device.id,
          storage_index: 1,
          storage_type: "fixed_disk",
          description: "/dev/sda1"
        })
        |> Repo.insert!()

      old_reading =
        %StorageReading{}
        |> StorageReading.changeset(%{
          storage_id: storage.id,
          used_bytes: 1000,
          total_bytes: 10_000,
          usage_percent: 10.0,
          checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
        })
        |> Repo.insert!()

      new_reading =
        %StorageReading{}
        |> StorageReading.changeset(%{
          storage_id: storage.id,
          used_bytes: 5000,
          total_bytes: 10_000,
          usage_percent: 50.0,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      latest = Snmp.get_latest_storage_reading(storage.id)
      assert latest.id == new_reading.id
      refute latest.id == old_reading.id
    end

    test "returns nil for storage with no readings", %{snmp_device: snmp_device} do
      storage =
        %Storage{}
        |> Storage.changeset(%{
          snmp_device_id: snmp_device.id,
          storage_index: 1,
          storage_type: "fixed_disk",
          description: "/dev/sda1"
        })
        |> Repo.insert!()

      assert Snmp.get_latest_storage_reading(storage.id) == nil
    end
  end

  describe "get_latest_storage_readings_batch/1" do
    test "returns empty map for empty storage list" do
      assert Snmp.get_latest_storage_readings_batch([]) == %{}
    end

    test "returns readings for multiple storage entries", %{snmp_device: snmp_device} do
      storage1 =
        %Storage{}
        |> Storage.changeset(%{
          snmp_device_id: snmp_device.id,
          storage_index: 1,
          storage_type: "fixed_disk",
          description: "/dev/sda1"
        })
        |> Repo.insert!()

      storage2 =
        %Storage{}
        |> Storage.changeset(%{
          snmp_device_id: snmp_device.id,
          storage_index: 2,
          storage_type: "ram",
          description: "Physical Memory"
        })
        |> Repo.insert!()

      reading1 =
        %StorageReading{}
        |> StorageReading.changeset(%{
          storage_id: storage1.id,
          used_bytes: 5000,
          total_bytes: 10_000,
          usage_percent: 50.0,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      reading2 =
        %StorageReading{}
        |> StorageReading.changeset(%{
          storage_id: storage2.id,
          used_bytes: 2000,
          total_bytes: 8000,
          usage_percent: 25.0,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      batch = Snmp.get_latest_storage_readings_batch([storage1.id, storage2.id])
      assert map_size(batch) == 2
      assert batch[storage1.id].id == reading1.id
      assert batch[storage2.id].id == reading2.id
    end

    test "returns only latest reading per storage", %{snmp_device: snmp_device} do
      storage =
        %Storage{}
        |> Storage.changeset(%{
          snmp_device_id: snmp_device.id,
          storage_index: 1,
          storage_type: "fixed_disk",
          description: "/dev/sda1"
        })
        |> Repo.insert!()

      _old_reading =
        %StorageReading{}
        |> StorageReading.changeset(%{
          storage_id: storage.id,
          used_bytes: 1000,
          total_bytes: 10_000,
          usage_percent: 10.0,
          checked_at: DateTime.add(DateTime.utc_now(), -3600, :second)
        })
        |> Repo.insert!()

      new_reading =
        %StorageReading{}
        |> StorageReading.changeset(%{
          storage_id: storage.id,
          used_bytes: 5000,
          total_bytes: 10_000,
          usage_percent: 50.0,
          checked_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      batch = Snmp.get_latest_storage_readings_batch([storage.id])
      assert map_size(batch) == 1
      assert batch[storage.id].id == new_reading.id
      assert batch[storage.id].usage_percent == 50.0
    end

    test "excludes storage without readings", %{snmp_device: snmp_device} do
      storage_with_reading =
        %Storage{}
        |> Storage.changeset(%{
          snmp_device_id: snmp_device.id,
          storage_index: 1,
          storage_type: "fixed_disk",
          description: "/dev/sda1"
        })
        |> Repo.insert!()

      storage_without_reading =
        %Storage{}
        |> Storage.changeset(%{
          snmp_device_id: snmp_device.id,
          storage_index: 2,
          storage_type: "ram",
          description: "Physical Memory"
        })
        |> Repo.insert!()

      %StorageReading{}
      |> StorageReading.changeset(%{
        storage_id: storage_with_reading.id,
        used_bytes: 5000,
        total_bytes: 10_000,
        usage_percent: 50.0,
        checked_at: DateTime.utc_now()
      })
      |> Repo.insert!()

      batch = Snmp.get_latest_storage_readings_batch([storage_with_reading.id, storage_without_reading.id])
      assert map_size(batch) == 1
      assert Map.has_key?(batch, storage_with_reading.id)
      refute Map.has_key?(batch, storage_without_reading.id)
    end
  end

  describe "create_storage_reading/1" do
    test "creates a storage reading with valid attributes", %{snmp_device: snmp_device} do
      storage =
        %Storage{}
        |> Storage.changeset(%{
          snmp_device_id: snmp_device.id,
          storage_index: 1,
          storage_type: "fixed_disk",
          description: "/dev/sda1"
        })
        |> Repo.insert!()

      {:ok, reading} =
        Snmp.create_storage_reading(%{
          storage_id: storage.id,
          used_bytes: 5000,
          total_bytes: 10_000,
          usage_percent: 50.0,
          checked_at: DateTime.utc_now()
        })

      assert reading.storage_id == storage.id
      assert reading.used_bytes == 5000
      assert reading.total_bytes == 10_000
      assert reading.usage_percent == 50.0
    end

    test "returns error with invalid attributes" do
      {:error, changeset} = Snmp.create_storage_reading(%{used_bytes: 1000})
      assert changeset.errors[:storage_id]
    end
  end

  describe "list_arp_entries/1" do
    test "returns all ARP entries for a device", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      arp =
        %ArpEntry{}
        |> ArpEntry.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          ip_address: "192.168.1.100",
          mac_address: "aa:bb:cc:dd:ee:ff",
          entry_type: "dynamic",
          if_index: 1,
          last_seen_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      entries = Snmp.list_arp_entries(device.id)
      assert length(entries) == 1
      assert hd(entries).id == arp.id
      assert Ecto.assoc_loaded?(hd(entries).interface)
    end

    test "returns empty list for device with no ARP entries", %{device: device} do
      assert Snmp.list_arp_entries(device.id) == []
    end

    test "orders ARP entries by IP address", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      arp2 =
        %ArpEntry{}
        |> ArpEntry.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          ip_address: "192.168.1.200",
          mac_address: "aa:bb:cc:dd:ee:02",
          last_seen_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      arp1 =
        %ArpEntry{}
        |> ArpEntry.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          ip_address: "192.168.1.100",
          mac_address: "aa:bb:cc:dd:ee:01",
          last_seen_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      entries = Snmp.list_arp_entries(device.id)
      assert length(entries) == 2
      assert Enum.at(entries, 0).id == arp1.id
      assert Enum.at(entries, 1).id == arp2.id
    end
  end

  describe "get_arp_entry/1" do
    test "returns ARP entry by id", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      arp =
        %ArpEntry{}
        |> ArpEntry.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          ip_address: "192.168.1.100",
          mac_address: "aa:bb:cc:dd:ee:ff",
          last_seen_at: DateTime.utc_now()
        })
        |> Repo.insert!()

      found = Snmp.get_arp_entry(arp.id)
      assert found.id == arp.id
      assert found.ip_address == "192.168.1.100"
    end

    test "returns nil for non-existent id" do
      assert Snmp.get_arp_entry(Ecto.UUID.generate()) == nil
    end
  end

  describe "upsert_arp_entry/1" do
    test "creates new ARP entry when none exists", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        ip_address: "192.168.1.100",
        mac_address: "aa:bb:cc:dd:ee:ff",
        entry_type: "dynamic",
        if_index: 1,
        last_seen_at: DateTime.utc_now()
      }

      {:ok, arp} = Snmp.upsert_arp_entry(attrs)
      assert arp.ip_address == "192.168.1.100"
      assert arp.mac_address == "aa:bb:cc:dd:ee:ff"
    end

    test "updates existing ARP entry when match found", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      existing =
        %ArpEntry{}
        |> ArpEntry.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          ip_address: "192.168.1.100",
          mac_address: "aa:bb:cc:dd:ee:ff",
          entry_type: "dynamic",
          if_index: 1,
          last_seen_at: DateTime.add(DateTime.utc_now(), -3600, :second)
        })
        |> Repo.insert!()

      new_time = DateTime.truncate(DateTime.utc_now(), :second)

      attrs = %{
        device_id: device.id,
        interface_id: interface.id,
        ip_address: "192.168.1.100",
        mac_address: "aa:bb:cc:dd:ee:ff",
        entry_type: "static",
        if_index: 1,
        last_seen_at: new_time
      }

      {:ok, updated} = Snmp.upsert_arp_entry(attrs)
      assert updated.id == existing.id
      assert updated.entry_type == "static"
    end
  end

  describe "delete_stale_arp_entries/2" do
    test "deletes ARP entries older than cutoff", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      old_time = DateTime.add(DateTime.utc_now(), -7200, :second)
      recent_time = DateTime.add(DateTime.utc_now(), -60, :second)

      old_arp =
        %ArpEntry{}
        |> ArpEntry.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          ip_address: "192.168.1.100",
          mac_address: "aa:bb:cc:dd:ee:ff",
          last_seen_at: old_time
        })
        |> Repo.insert!()

      recent_arp =
        %ArpEntry{}
        |> ArpEntry.changeset(%{
          device_id: device.id,
          interface_id: interface.id,
          ip_address: "192.168.1.200",
          mac_address: "11:22:33:44:55:66",
          last_seen_at: recent_time
        })
        |> Repo.insert!()

      cutoff = DateTime.add(DateTime.utc_now(), -3600, :second)
      {deleted_count, _} = Snmp.delete_stale_arp_entries(device.id, cutoff)

      assert deleted_count == 1
      assert Snmp.get_arp_entry(old_arp.id) == nil
      assert Snmp.get_arp_entry(recent_arp.id)
    end

    test "does not delete ARP entries from other devices", %{organization: organization} do
      {:ok, site} =
        Towerops.Sites.create_site(%{
          name: "Another Site",
          organization_id: organization.id
        })

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

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

      old_time = DateTime.add(DateTime.utc_now(), -7200, :second)

      arp1 =
        %ArpEntry{}
        |> ArpEntry.changeset(%{
          device_id: device1.id,
          ip_address: "192.168.1.100",
          mac_address: "aa:bb:cc:dd:ee:ff",
          last_seen_at: old_time
        })
        |> Repo.insert!()

      arp2 =
        %ArpEntry{}
        |> ArpEntry.changeset(%{
          device_id: device2.id,
          ip_address: "192.168.1.100",
          mac_address: "11:22:33:44:55:66",
          last_seen_at: old_time
        })
        |> Repo.insert!()

      cutoff = DateTime.add(DateTime.utc_now(), -3600, :second)
      {deleted_count, _} = Snmp.delete_stale_arp_entries(device1.id, cutoff)

      assert deleted_count == 1
      assert Snmp.get_arp_entry(arp1.id) == nil
      assert Snmp.get_arp_entry(arp2.id)
    end
  end

  describe "upsert_arp_entries/3" do
    test "creates multiple ARP entries with interface mapping", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      arp_data = [
        %{
          ip_address: "192.168.1.100",
          mac_address: "aa:bb:cc:dd:ee:ff",
          if_index: 1,
          entry_type: "dynamic",
          last_seen_at: DateTime.utc_now()
        },
        %{
          ip_address: "192.168.1.200",
          mac_address: "11:22:33:44:55:66",
          if_index: 1,
          entry_type: "static",
          last_seen_at: DateTime.utc_now()
        }
      ]

      {success, errors} = Snmp.upsert_arp_entries(device.id, arp_data, [interface])
      assert success == 2
      assert errors == 0

      entries = Snmp.list_arp_entries(device.id)
      assert length(entries) == 2

      ips = entries |> Enum.map(& &1.ip_address) |> Enum.sort()
      assert ips == ["192.168.1.100", "192.168.1.200"]

      # Both should have the interface linked
      assert Enum.all?(entries, &(&1.interface_id == interface.id))
    end

    test "handles entries without matching interface", %{device: device} do
      arp_data = [
        %{
          ip_address: "192.168.1.100",
          mac_address: "aa:bb:cc:dd:ee:ff",
          if_index: 999,
          entry_type: "dynamic",
          last_seen_at: DateTime.utc_now()
        }
      ]

      {success, errors} = Snmp.upsert_arp_entries(device.id, arp_data, [])
      assert success == 1
      assert errors == 0

      entries = Snmp.list_arp_entries(device.id)
      assert length(entries) == 1
      assert hd(entries).interface_id == nil
      assert hd(entries).if_index == 999
    end
  end

  describe "upsert_arp_entries/3 error tracking" do
    test "returns success and error counts", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      arp_data = [
        %{
          ip_address: "192.168.1.100",
          mac_address: "aa:bb:cc:dd:ee:ff",
          if_index: 1,
          entry_type: "dynamic",
          last_seen_at: DateTime.utc_now()
        },
        %{
          ip_address: "192.168.1.200",
          mac_address: "11:22:33:44:55:66",
          if_index: 1,
          entry_type: "static",
          last_seen_at: DateTime.utc_now()
        }
      ]

      {success, errors} = Snmp.upsert_arp_entries(device.id, arp_data, [interface])
      assert success == 2
      assert errors == 0
    end
  end

  describe "upsert_mac_addresses/3 error tracking" do
    test "returns success and error counts", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      mac_data = [
        %{
          mac_address: "aa:bb:cc:dd:ee:ff",
          port_index: 1,
          vlan_id: 1,
          last_seen_at: DateTime.utc_now()
        },
        %{
          mac_address: "11:22:33:44:55:66",
          port_index: 1,
          vlan_id: 1,
          last_seen_at: DateTime.utc_now()
        }
      ]

      {success, errors} = Snmp.upsert_mac_addresses(device.id, mac_data, [interface])
      assert success == 2
      assert errors == 0
    end
  end

  describe "list_discovered_devices_for_organization/1" do
    test "merges data from neighbors, ARP, and MAC tables", %{
      organization: organization,
      device: device,
      snmp_device: snmp_device
    } do
      # Create an interface
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      # Create a neighbor entry
      %Neighbor{}
      |> Neighbor.changeset(%{
        device_id: device.id,
        snmp_device_id: snmp_device.id,
        interface_id: interface.id,
        remote_chassis_id: "aa:bb:cc:dd:ee:ff",
        remote_port_id: "Gi1/0/1",
        remote_system_name: "discovered-switch",
        remote_platform: "Cisco IOS Software",
        remote_capabilities: ["bridge", "router"],
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      })
      |> Repo.insert!()

      # Create an ARP entry with same MAC
      %ArpEntry{}
      |> ArpEntry.changeset(%{
        device_id: device.id,
        ip_address: "192.168.1.100",
        mac_address: "aa:bb:cc:dd:ee:ff",
        if_index: 1,
        entry_type: "dynamic",
        last_seen_at: DateTime.utc_now()
      })
      |> Repo.insert!()

      discovered = Snmp.list_discovered_devices_for_organization(organization.id)

      assert length(discovered) == 1
      entry = hd(discovered)

      # Should merge data from both sources
      assert entry.identifier.type == :mac
      assert entry.identifier.value == "aa:bb:cc:dd:ee:ff"
      assert entry.hostname == "discovered-switch"
      assert "192.168.1.100" in entry.ip_addresses
      assert "aa:bb:cc:dd:ee:ff" in entry.mac_addresses
      assert entry.source_count == 2
      assert :lldp in entry.protocols_used
      assert :arp in entry.protocols_used
    end

    test "filters out existing devices by IP address", %{
      organization: organization,
      device: device,
      snmp_device: snmp_device
    } do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      # Create neighbor with same IP as existing device
      %Neighbor{}
      |> Neighbor.changeset(%{
        device_id: device.id,
        snmp_device_id: snmp_device.id,
        interface_id: interface.id,
        remote_chassis_id: "aa:bb:cc:dd:ee:ff",
        remote_port_id: "Gi1/0/1",
        remote_system_name: "test-router",
        remote_address: device.ip_address,
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      })
      |> Repo.insert!()

      discovered = Snmp.list_discovered_devices_for_organization(organization.id)

      # Should be filtered out
      assert discovered == []
    end

    test "filters out existing devices by MAC address", %{
      organization: organization,
      device: device,
      snmp_device: snmp_device
    } do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0",
          if_phys_address: "aa:bb:cc:dd:ee:ff"
        })
        |> Repo.insert!()

      # Create neighbor with same MAC as existing interface
      %Neighbor{}
      |> Neighbor.changeset(%{
        device_id: device.id,
        snmp_device_id: snmp_device.id,
        interface_id: interface.id,
        remote_chassis_id: "aa:bb:cc:dd:ee:ff",
        remote_port_id: "Gi1/0/1",
        remote_system_name: "discovered-device",
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      })
      |> Repo.insert!()

      discovered = Snmp.list_discovered_devices_for_organization(organization.id)

      # Should be filtered out
      assert discovered == []
    end

    test "infers device type from LLDP capabilities", %{
      organization: organization,
      device: device,
      snmp_device: snmp_device
    } do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      # Create neighbor with bridge capability
      %Neighbor{}
      |> Neighbor.changeset(%{
        device_id: device.id,
        snmp_device_id: snmp_device.id,
        interface_id: interface.id,
        remote_chassis_id: "aa:bb:cc:dd:ee:ff",
        remote_port_id: "Gi1/0/1",
        remote_system_name: "switch-device",
        remote_capabilities: ["bridge"],
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      })
      |> Repo.insert!()

      discovered = Snmp.list_discovered_devices_for_organization(organization.id)

      assert length(discovered) == 1
      assert hd(discovered).device_type == :switch
    end

    test "infers device type from platform description", %{
      organization: organization,
      device: device,
      snmp_device: snmp_device
    } do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      # Create neighbor with Cisco IOS description
      %Neighbor{}
      |> Neighbor.changeset(%{
        device_id: device.id,
        snmp_device_id: snmp_device.id,
        interface_id: interface.id,
        remote_chassis_id: "aa:bb:cc:dd:ee:ff",
        remote_port_id: "Gi1/0/1",
        remote_system_name: "router-device",
        remote_platform: "Cisco IOS Software, Version 15.0",
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      })
      |> Repo.insert!()

      discovered = Snmp.list_discovered_devices_for_organization(organization.id)

      assert length(discovered) == 1
      assert hd(discovered).device_type == :router
    end

    test "prefers MAC address as primary identifier", %{
      organization: organization,
      device: device,
      snmp_device: snmp_device
    } do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      # Create neighbor with both MAC and IP
      %Neighbor{}
      |> Neighbor.changeset(%{
        device_id: device.id,
        snmp_device_id: snmp_device.id,
        interface_id: interface.id,
        remote_chassis_id: "aa:bb:cc:dd:ee:ff",
        remote_port_id: "Gi1/0/1",
        remote_system_name: "test-device",
        remote_address: "192.168.1.100",
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      })
      |> Repo.insert!()

      discovered = Snmp.list_discovered_devices_for_organization(organization.id)

      assert length(discovered) == 1
      entry = hd(discovered)

      # Should prefer MAC as identifier
      assert entry.identifier.type == :mac
      assert entry.identifier.value == "aa:bb:cc:dd:ee:ff"
    end

    test "returns empty list when no discovery data exists", %{organization: organization} do
      discovered = Snmp.list_discovered_devices_for_organization(organization.id)
      assert discovered == []
    end

    test "sorts by source count and last seen", %{organization: organization, device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      # Create one neighbor with single source (older)
      old_time = DateTime.add(DateTime.utc_now(), -3600, :second)

      %Neighbor{}
      |> Neighbor.changeset(%{
        device_id: device.id,
        snmp_device_id: snmp_device.id,
        interface_id: interface.id,
        remote_chassis_id: "11:11:11:11:11:11",
        remote_port_id: "Gi1/0/1",
        remote_system_name: "old-device",
        protocol: "lldp",
        last_discovered_at: old_time
      })
      |> Repo.insert!()

      # Create another neighbor with single source (newer)
      %Neighbor{}
      |> Neighbor.changeset(%{
        device_id: device.id,
        snmp_device_id: snmp_device.id,
        interface_id: interface.id,
        remote_chassis_id: "22:22:22:22:22:22",
        remote_port_id: "Gi1/0/2",
        remote_system_name: "new-device",
        protocol: "lldp",
        last_discovered_at: DateTime.utc_now()
      })
      |> Repo.insert!()

      # Create a device with multiple sources
      %Neighbor{}
      |> Neighbor.changeset(%{
        device_id: device.id,
        snmp_device_id: snmp_device.id,
        interface_id: interface.id,
        remote_chassis_id: "33:33:33:33:33:33",
        remote_port_id: "Gi1/0/3",
        remote_system_name: "multi-source",
        protocol: "lldp",
        last_discovered_at: old_time
      })
      |> Repo.insert!()

      %ArpEntry{}
      |> ArpEntry.changeset(%{
        device_id: device.id,
        ip_address: "192.168.1.100",
        mac_address: "33:33:33:33:33:33",
        if_index: 1,
        entry_type: "dynamic",
        last_seen_at: old_time
      })
      |> Repo.insert!()

      discovered = Snmp.list_discovered_devices_for_organization(organization.id)

      assert length(discovered) == 3

      # First should be multi-source (source_count = 2)
      assert hd(discovered).hostname == "multi-source"
      assert hd(discovered).source_count == 2

      # Next two should be sorted by last_seen (newer first)
      assert Enum.at(discovered, 1).hostname == "new-device"
      assert Enum.at(discovered, 2).hostname == "old-device"
    end
  end

  describe "concurrent upsert operations (race condition tests)" do
    test "concurrent neighbor upserts don't create duplicates", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      # Spawn 10 concurrent workers all upserting the same neighbor
      tasks =
        for i <- 1..10 do
          Task.async(fn ->
            Snmp.upsert_neighbor(%{
              device_id: device.id,
              interface_id: interface.id,
              remote_chassis_id: "00:11:22:33:44:55",
              protocol: "lldp",
              remote_system_name: "neighbor-#{i}",
              last_discovered_at: DateTime.utc_now()
            })
          end)
        end

      # Wait for all tasks to complete
      results = Task.await_many(tasks, 5_000)

      # All should succeed
      assert Enum.all?(results, fn
               {:ok, _} -> true
               _ -> false
             end)

      # Should have exactly 1 neighbor, not 10
      neighbors =
        Repo.all(
          from n in Neighbor,
            where: n.interface_id == ^interface.id and n.remote_chassis_id == "00:11:22:33:44:55"
        )

      assert length(neighbors) == 1

      # The final name should be one of the concurrent writes (non-deterministic but valid)
      neighbor = hd(neighbors)
      assert String.starts_with?(neighbor.remote_system_name, "neighbor-")
    end

    test "concurrent ARP entry upserts don't create duplicates", %{device: device} do
      # Spawn 10 concurrent workers all upserting the same ARP entry
      tasks =
        for _i <- 1..10 do
          Task.async(fn ->
            Snmp.upsert_arp_entry(%{
              device_id: device.id,
              ip_address: "192.168.1.100",
              mac_address: "aa:bb:cc:dd:ee:ff",
              entry_type: "dynamic",
              last_seen_at: DateTime.utc_now()
            })
          end)
        end

      # Wait for all tasks to complete
      results = Task.await_many(tasks, 5_000)

      # All should succeed
      assert Enum.all?(results, fn
               {:ok, _} -> true
               _ -> false
             end)

      # Should have exactly 1 ARP entry, not 10
      arp_entries =
        Repo.all(
          from a in ArpEntry,
            where:
              a.device_id == ^device.id and a.ip_address == "192.168.1.100" and
                a.mac_address == "aa:bb:cc:dd:ee:ff"
        )

      assert length(arp_entries) == 1
    end

    test "concurrent MAC address upserts don't create duplicates", %{device: device} do
      # Spawn 10 concurrent workers all upserting the same MAC address
      tasks =
        for i <- 1..10 do
          Task.async(fn ->
            Snmp.upsert_mac_address(%{
              device_id: device.id,
              mac_address: "11:22:33:44:55:66",
              vlan_id: 100,
              port_index: i,
              last_seen_at: DateTime.utc_now()
            })
          end)
        end

      # Wait for all tasks to complete
      results = Task.await_many(tasks, 5_000)

      # All should succeed
      assert Enum.all?(results, fn
               {:ok, _} -> true
               _ -> false
             end)

      # Should have exactly 1 MAC address, not 10
      mac_addresses =
        Repo.all(
          from m in MacAddress,
            where: m.device_id == ^device.id and m.mac_address == "11:22:33:44:55:66" and m.vlan_id == 100
        )

      assert length(mac_addresses) == 1

      # The final port_index should be one of the concurrent writes
      mac = hd(mac_addresses)
      assert mac.port_index in 1..10
    end

    test "concurrent MAC address upserts with nil vlan_id don't create duplicates", %{device: device} do
      # Spawn 10 concurrent workers all upserting the same MAC address with nil vlan_id
      tasks =
        for i <- 1..10 do
          Task.async(fn ->
            Snmp.upsert_mac_address(%{
              device_id: device.id,
              mac_address: "aa:bb:cc:dd:ee:ff",
              vlan_id: nil,
              port_index: i,
              last_seen_at: DateTime.utc_now()
            })
          end)
        end

      # Wait for all tasks to complete
      results = Task.await_many(tasks, 5_000)

      # All should succeed
      assert Enum.all?(results, fn
               {:ok, _} -> true
               _ -> false
             end)

      # Should have exactly 1 MAC address, not 10
      mac_addresses =
        Repo.all(
          from m in MacAddress,
            where: m.device_id == ^device.id and m.mac_address == "aa:bb:cc:dd:ee:ff" and is_nil(m.vlan_id)
        )

      assert length(mac_addresses) == 1
    end

    test "concurrent neighbor upserts with nil chassis_id handled correctly", %{
      device: device,
      snmp_device: snmp_device
    } do
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_name: "eth0"
        })
        |> Repo.insert!()

      # Spawn 10 concurrent workers all upserting neighbors with nil chassis_id
      tasks =
        for i <- 1..10 do
          Task.async(fn ->
            Snmp.upsert_neighbor(%{
              device_id: device.id,
              interface_id: interface.id,
              remote_chassis_id: nil,
              protocol: "cdp",
              remote_system_name: "neighbor-#{i}",
              last_discovered_at: DateTime.utc_now()
            })
          end)
        end

      # Wait for all tasks to complete
      results = Task.await_many(tasks, 5_000)

      # All should succeed
      assert Enum.all?(results, fn
               {:ok, _} -> true
               _ -> false
             end)

      # Should have exactly 1 neighbor with nil chassis_id
      neighbors =
        Repo.all(
          from n in Neighbor,
            where: n.interface_id == ^interface.id and is_nil(n.remote_chassis_id) and n.protocol == "cdp"
        )

      assert length(neighbors) == 1
    end
  end

  describe "delete_stale_and_upsert_neighbors/3" do
    test "atomically deletes stale and upserts new neighbors", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{snmp_device_id: snmp_device.id, if_index: 1, if_name: "eth0"})
        |> Repo.insert!()

      cutoff = DateTime.add(DateTime.utc_now(), -5, :minute)

      # Create a stale neighbor (old timestamp)
      {:ok, _stale} =
        Snmp.upsert_neighbor(%{
          device_id: device.id,
          interface_id: interface.id,
          remote_chassis_id: "stale-chassis",
          remote_port_id: "ge0/0/1",
          remote_system_name: "stale-switch",
          protocol: "lldp",
          last_discovered_at: DateTime.add(DateTime.utc_now(), -10, :minute)
        })

      # New neighbors to upsert
      new_neighbors = [
        %{
          device_id: device.id,
          interface_id: interface.id,
          remote_chassis_id: "new-chassis",
          remote_port_id: "ge0/0/2",
          remote_system_name: "new-switch",
          protocol: "lldp",
          last_discovered_at: DateTime.utc_now()
        }
      ]

      assert {:ok, _} = Snmp.delete_stale_and_upsert_neighbors(device.id, new_neighbors, cutoff)

      remaining = Repo.all(from n in Neighbor, where: n.device_id == ^device.id)
      assert length(remaining) == 1
      assert hd(remaining).remote_chassis_id == "new-chassis"
    end
  end

  describe "delete_stale_and_upsert_arp_entries/4" do
    test "atomically deletes stale and upserts new ARP entries", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{snmp_device_id: snmp_device.id, if_index: 1, if_name: "eth0"})
        |> Repo.insert!()

      now = DateTime.truncate(DateTime.utc_now(), :second)
      cutoff = DateTime.add(now, -5, :minute)

      # Create a stale ARP entry
      Repo.insert!(%ArpEntry{
        device_id: device.id,
        interface_id: interface.id,
        ip_address: "10.0.0.99",
        mac_address: "aa:bb:cc:dd:ee:ff",
        last_seen_at: DateTime.add(now, -10, :minute)
      })

      # New entries to upsert
      new_entries = [
        %{
          ip_address: "10.0.0.100",
          mac_address: "11:22:33:44:55:66",
          if_index: interface.if_index,
          last_seen_at: now
        }
      ]

      assert {:ok, _} =
               Snmp.delete_stale_and_upsert_arp_entries(
                 device.id,
                 new_entries,
                 [interface],
                 cutoff
               )

      remaining = Repo.all(from a in ArpEntry, where: a.device_id == ^device.id)
      assert length(remaining) == 1
      assert hd(remaining).ip_address == "10.0.0.100"
    end
  end

  describe "delete_stale_and_upsert_mac_addresses/4" do
    test "atomically deletes stale and upserts new MAC addresses", %{device: device, snmp_device: snmp_device} do
      interface =
        %Interface{}
        |> Interface.changeset(%{snmp_device_id: snmp_device.id, if_index: 1, if_name: "eth0"})
        |> Repo.insert!()

      now = DateTime.truncate(DateTime.utc_now(), :second)
      cutoff = DateTime.add(now, -5, :minute)

      # Create a stale MAC entry
      Repo.insert!(%MacAddress{
        device_id: device.id,
        interface_id: interface.id,
        mac_address: "aa:bb:cc:dd:ee:ff",
        vlan_id: 1,
        last_seen_at: DateTime.add(now, -10, :minute)
      })

      # New entries to upsert
      new_entries = [
        %{
          mac_address: "11:22:33:44:55:66",
          port_index: interface.if_index,
          vlan_id: 1,
          last_seen_at: now
        }
      ]

      assert {:ok, _} =
               Snmp.delete_stale_and_upsert_mac_addresses(
                 device.id,
                 new_entries,
                 [interface],
                 cutoff
               )

      remaining = Repo.all(from m in MacAddress, where: m.device_id == ^device.id)
      assert length(remaining) == 1
      assert hd(remaining).mac_address == "11:22:33:44:55:66"
    end
  end

  describe "create_checks_from_discovery/2" do
    test "creates checks for sensors", %{device: device, snmp_device: snmp_device} do
      # Create a sensor to discover
      sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_descr: "CPU Temperature",
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: ".1.3.6.1.4.1.9.9.13.1.3.1.3.1",
          sensor_unit: "°C",
          sensor_divisor: 1
        })
        |> Repo.insert!()

      # Run check creation
      assert {:ok, counts} = Snmp.create_checks_from_discovery(device, snmp_device)
      assert counts.sensors == 1

      # Verify check was created
      checks = Towerops.Monitoring.list_checks(device.organization_id, device_id: device.id)
      assert length(checks) == 1

      check = hd(checks)
      assert check.name == "CPU Temperature"
      assert check.check_type == "snmp_sensor"
      assert check.source_type == "auto_discovery"
      assert check.source_id == sensor.id
      assert check.device_id == device.id
      assert check.enabled == true
      assert check.interval_seconds == 60

      # Verify config contains sensor metadata
      assert check.config["sensor_type"] == "temperature"
      assert check.config["sensor_oid"] == ".1.3.6.1.4.1.9.9.13.1.3.1.3.1"
    end

    test "creates checks for interfaces", %{device: device, snmp_device: snmp_device} do
      # Create an interface
      interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_descr: "eth0",
          if_name: "Ethernet0",
          if_type: 6,
          if_speed: 1_000_000_000,
          if_oper_status: "up"
        })
        |> Repo.insert!()

      assert {:ok, counts} = Snmp.create_checks_from_discovery(device, snmp_device)
      assert counts.interfaces == 1

      checks = Towerops.Monitoring.list_checks(device.organization_id, device_id: device.id)
      assert length(checks) == 1

      check = hd(checks)
      assert check.name == "Ethernet0 Status"
      assert check.check_type == "snmp_interface"
      assert check.source_type == "auto_discovery"
      assert check.source_id == interface.id
      assert check.config["if_index"] == 1
      assert check.config["if_descr"] == "eth0"
    end

    test "creates checks for processors", %{device: device, snmp_device: snmp_device} do
      # Create a processor
      processor =
        %Processor{}
        |> Processor.changeset(%{
          snmp_device_id: snmp_device.id,
          processor_index: "1",
          description: "CPU 1",
          processor_type: "hr_processor"
        })
        |> Repo.insert!()

      assert {:ok, counts} = Snmp.create_checks_from_discovery(device, snmp_device)
      assert counts.processors == 1

      checks = Towerops.Monitoring.list_checks(device.organization_id, device_id: device.id)
      check = hd(checks)
      assert check.name == "CPU 1"
      assert check.check_type == "snmp_processor"
      assert check.source_id == processor.id
    end

    test "creates checks for storage", %{device: device, snmp_device: snmp_device} do
      # Create storage
      storage =
        %Storage{}
        |> Storage.changeset(%{
          snmp_device_id: snmp_device.id,
          storage_index: 1,
          description: "/ (root)",
          storage_type: "fixed_disk",
          total_bytes: 100_000_000,
          used_bytes: 50_000_000
        })
        |> Repo.insert!()

      assert {:ok, counts} = Snmp.create_checks_from_discovery(device, snmp_device)
      assert counts.storage == 1

      checks = Towerops.Monitoring.list_checks(device.organization_id, device_id: device.id)
      check = hd(checks)
      assert check.name == "/ (root) Usage"
      assert check.check_type == "snmp_storage"
      assert check.source_id == storage.id
    end

    test "creates multiple checks for different types", %{device: device, snmp_device: snmp_device} do
      # Create multiple items
      _sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_type: "temperature",
          sensor_descr: "Temperature 1",
          sensor_index: "1",
          sensor_oid: ".1.2.3.4",
          sensor_unit: "°C",
          sensor_divisor: 1
        })
        |> Repo.insert!()

      _interface =
        %Interface{}
        |> Interface.changeset(%{
          snmp_device_id: snmp_device.id,
          if_index: 1,
          if_descr: "eth0",
          if_oper_status: "up"
        })
        |> Repo.insert!()

      assert {:ok, counts} = Snmp.create_checks_from_discovery(device, snmp_device)
      assert counts.sensors == 1
      assert counts.interfaces == 1
      assert counts.processors == 0
      assert counts.storage == 0

      checks = Towerops.Monitoring.list_checks(device.organization_id, device_id: device.id)
      assert length(checks) == 2

      check_types = checks |> Enum.map(& &1.check_type) |> Enum.sort()
      assert check_types == ["snmp_interface", "snmp_sensor"]
    end

    test "schedules checks for execution", %{device: device, snmp_device: snmp_device} do
      _sensor =
        %Sensor{}
        |> Sensor.changeset(%{
          snmp_device_id: snmp_device.id,
          sensor_descr: "Test Sensor",
          sensor_type: "temperature",
          sensor_index: "1",
          sensor_oid: ".1.2.3.4",
          sensor_unit: "°C",
          sensor_divisor: 1
        })
        |> Repo.insert!()

      assert {:ok, _counts} = Snmp.create_checks_from_discovery(device, snmp_device)

      # Verify Oban job was scheduled
      jobs = Repo.all(from j in Oban.Job, where: j.worker == "Towerops.Workers.CheckExecutorWorker")
      assert length(jobs) == 1

      job = hd(jobs)
      assert job.worker == "Towerops.Workers.CheckExecutorWorker"
      assert job.queue == "check_executors"
    end
  end
end
