defmodule Towerops.Snmp.Profiles.BaseTest do
  use Towerops.DataCase, async: true

  import Mox

  alias Towerops.Snmp.Profiles.Base
  alias Towerops.Snmp.SnmpMock

  setup :verify_on_exit!

  @client_opts [
    ip: "192.168.1.1",
    community: "public",
    version: "2c",
    port: 161,
    timeout: 5000
  ]

  describe "discover_system_info/1" do
    test "successfully discovers system information" do
      expect(SnmpMock, :get, 6, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.1.1.0" ->
            {:ok, {:octet_string, "Linux server 5.4.0-42-generic"}}

          "1.3.6.1.2.1.1.2.0" ->
            {:ok, {:object_identifier, [1, 3, 6, 1, 4, 1, 8072, 3, 2, 10]}}

          "1.3.6.1.2.1.1.3.0" ->
            {:ok, {:timeticks, 123_456_789}}

          "1.3.6.1.2.1.1.4.0" ->
            {:ok, {:octet_string, "admin@example.com"}}

          "1.3.6.1.2.1.1.5.0" ->
            {:ok, {:octet_string, "webserver01"}}

          "1.3.6.1.2.1.1.6.0" ->
            {:ok, {:octet_string, "DC1 Rack 42"}}

          _ ->
            {:error, :no_such_object}
        end
      end)

      assert {:ok, system_info} = Base.discover_system_info(@client_opts)

      assert system_info.sys_descr == "Linux server 5.4.0-42-generic"
      assert system_info.sys_object_id == "1.3.6.1.4.1.8072.3.2.10"
      assert system_info.sys_name == "webserver01"
      assert system_info.sys_uptime == 123_456_789
      assert system_info.sys_contact == "admin@example.com"
      assert system_info.sys_location == "DC1 Rack 42"
    end

    test "handles partial failure gracefully" do
      expect(SnmpMock, :get, 6, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.1.1.0" ->
            {:ok, {:octet_string, "Test Device"}}

          _ ->
            {:error, :no_such_object}
        end
      end)

      # Should fail because get_multiple returns error on any failure
      assert {:error, :partial_failure} = Base.discover_system_info(@client_opts)
    end

    test "handles timeout errors" do
      expect(SnmpMock, :get, 6, fn _, _, _ ->
        {:error, :timeout}
      end)

      assert {:error, :partial_failure} = Base.discover_system_info(@client_opts)
    end
  end

  describe "discover_interfaces/1" do
    test "successfully discovers multiple interfaces" do
      # Mock ifIndex walk
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.1", _ ->
        {:ok,
         [
           %{oid: "1.3.6.1.2.1.2.2.1.1.1", value: {:integer, 1}},
           %{oid: "1.3.6.1.2.1.2.2.1.1.2", value: {:integer, 2}},
           %{oid: "1.3.6.1.2.1.2.2.1.1.3", value: {:integer, 3}}
         ]}
      end)

      # Mock interface data for each interface (9 OIDs per interface × 3 interfaces = 27 calls)
      # OIDs: ifDescr, ifType, ifSpeed, ifPhysAddress, ifAdminStatus, ifOperStatus, ifName, ifHighSpeed, ifAlias
      expect(SnmpMock, :get, 27, fn _, oid, _ ->
        cond do
          String.ends_with?(oid, ".1") ->
            parts = String.split(oid, ".")
            second_to_last = Enum.at(parts, -2)

            cond do
              second_to_last == "2" -> {:ok, {:octet_string, "eth0"}}
              second_to_last == "3" -> {:ok, {:integer, 6}}
              second_to_last == "5" -> {:ok, {:gauge32, 1_000_000_000}}
              second_to_last == "6" -> {:ok, {:octet_string, <<0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x01>>}}
              second_to_last == "7" -> {:ok, {:integer, 1}}
              second_to_last == "8" -> {:ok, {:integer, 1}}
              second_to_last == "1" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "eth0"}}
              second_to_last == "15" and String.contains?(oid, "31.1.1.1") -> {:ok, {:gauge32, 1000}}
              second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "WAN"}}
              true -> {:error, :no_such_object}
            end

          String.ends_with?(oid, ".2") ->
            parts = String.split(oid, ".")
            second_to_last = Enum.at(parts, -2)

            cond do
              second_to_last == "2" -> {:ok, {:octet_string, "eth1"}}
              second_to_last == "3" -> {:ok, {:integer, 6}}
              second_to_last == "5" -> {:ok, {:gauge32, 100_000_000}}
              second_to_last == "6" -> {:ok, {:octet_string, <<0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0x02>>}}
              second_to_last == "7" -> {:ok, {:integer, 1}}
              second_to_last == "8" -> {:ok, {:integer, 2}}
              second_to_last == "1" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "eth1"}}
              second_to_last == "15" and String.contains?(oid, "31.1.1.1") -> {:ok, {:gauge32, 100}}
              second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "LAN"}}
              true -> {:error, :no_such_object}
            end

          String.ends_with?(oid, ".3") ->
            parts = String.split(oid, ".")
            second_to_last = Enum.at(parts, -2)

            cond do
              second_to_last == "2" -> {:ok, {:octet_string, "lo"}}
              second_to_last == "3" -> {:ok, {:integer, 24}}
              second_to_last == "5" -> {:ok, {:gauge32, 10_000_000}}
              second_to_last == "6" -> {:ok, {:octet_string, <<>>}}
              second_to_last == "7" -> {:ok, {:integer, 1}}
              second_to_last == "8" -> {:ok, {:integer, 1}}
              second_to_last == "1" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "lo"}}
              second_to_last == "15" and String.contains?(oid, "31.1.1.1") -> {:ok, {:gauge32, 10}}
              second_to_last == "18" and String.contains?(oid, "31.1.1.1") -> {:ok, {:octet_string, "Loopback"}}
              true -> {:error, :no_such_object}
            end

          true ->
            {:error, :no_such_object}
        end
      end)

      assert {:ok, interfaces} = Base.discover_interfaces(@client_opts)

      assert length(interfaces) == 3

      eth0 = Enum.find(interfaces, &(&1.if_index == 1))
      assert eth0.if_descr == "eth0"
      assert eth0.if_name == "eth0"
      assert eth0.if_alias == "WAN"
      assert eth0.if_type == 6
      assert eth0.if_speed == 1_000_000_000
      assert eth0.if_phys_address == "aa:bb:cc:dd:ee:01"
      assert eth0.if_admin_status == "up"
      assert eth0.if_oper_status == "up"

      eth1 = Enum.find(interfaces, &(&1.if_index == 2))
      assert eth1.if_descr == "eth1"
      assert eth1.if_oper_status == "down"

      lo = Enum.find(interfaces, &(&1.if_index == 3))
      assert lo.if_descr == "lo"
      assert lo.if_phys_address == nil
    end

    test "handles empty interface list" do
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.1", _ ->
        {:ok, []}
      end)

      assert {:ok, interfaces} = Base.discover_interfaces(@client_opts)
      assert interfaces == []
    end

    test "handles walk failure" do
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.1", _ ->
        {:error, :timeout}
      end)

      assert {:error, :timeout} = Base.discover_interfaces(@client_opts)
    end
  end

  describe "discover_sensors/1" do
    test "discovers sensors from ENTITY-SENSOR-MIB" do
      # Mock sensor type walk and ENTITY-MIB description walks
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.99.1.1.1.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.99.1.1.1.1.1", value: {:integer, 8}},
               %{oid: "1.3.6.1.2.1.99.1.1.1.1.2", value: {:integer, 3}}
             ]}

          # ENTITY-MIB walks for descriptions (return empty for simplicity)
          _ ->
            {:ok, []}
        end
      end)

      # Mock sensor data for each sensor (5 OIDs per sensor × 2 sensors = 10 calls)
      stub(SnmpMock, :get, fn _, oid, _ ->
        cond do
          String.ends_with?(oid, ".1") ->
            case oid |> String.split(".") |> Enum.at(-2) do
              "1" -> {:ok, {:integer, 8}}
              "2" -> {:ok, {:integer, 9}}
              "3" -> {:ok, {:integer, 0}}
              "4" -> {:ok, {:integer, 45}}
              "5" -> {:ok, {:integer, 1}}
              _ -> {:error, :no_such_object}
            end

          String.ends_with?(oid, ".2") ->
            case oid |> String.split(".") |> Enum.at(-2) do
              "1" -> {:ok, {:integer, 3}}
              "2" -> {:ok, {:integer, 6}}
              "3" -> {:ok, {:integer, 3}}
              "4" -> {:ok, {:integer, 12_000}}
              "5" -> {:ok, {:integer, 1}}
              _ -> {:error, :no_such_object}
            end

          true ->
            {:error, :no_such_object}
        end
      end)

      assert {:ok, sensors} = Base.discover_sensors(@client_opts)

      assert length(sensors) == 2

      temp_sensor = Enum.find(sensors, &(&1.sensor_index == "1"))
      assert temp_sensor.sensor_type == "celsius"
      assert temp_sensor.sensor_unit == "°C"
      assert temp_sensor.last_value == 45.0
      assert temp_sensor.status == "ok"

      voltage_sensor = Enum.find(sensors, &(&1.sensor_index == "2"))
      assert voltage_sensor.sensor_type == "volts"
      assert voltage_sensor.sensor_unit == "V"
      assert voltage_sensor.last_value == 12_000.0
    end

    test "returns empty list when ENTITY-SENSOR-MIB not supported" do
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.99.1.1.1.1", _ ->
        {:ok, []}
      end)

      assert {:ok, sensors} = Base.discover_sensors(@client_opts)
      assert sensors == []
    end

    test "returns empty list on walk error" do
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.99.1.1.1.1", _ ->
        {:error, :no_such_object}
      end)

      assert {:ok, sensors} = Base.discover_sensors(@client_opts)
      assert sensors == []
    end

    test "filters out sensors with missing data" do
      # Mock sensor type walk and ENTITY-MIB description walks
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.99.1.1.1.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.99.1.1.1.1.1", value: {:integer, 8}},
               %{oid: "1.3.6.1.2.1.99.1.1.1.1.2", value: {:integer, 8}}
             ]}

          _ ->
            {:ok, []}
        end
      end)

      # First sensor succeeds, second fails (5 OIDs per sensor)
      stub(SnmpMock, :get, fn _, oid, _ ->
        if String.ends_with?(oid, ".1") do
          case oid |> String.split(".") |> Enum.at(-2) do
            "1" -> {:ok, {:integer, 8}}
            "2" -> {:ok, {:integer, 9}}
            "3" -> {:ok, {:integer, 0}}
            "4" -> {:ok, {:integer, 42}}
            "5" -> {:ok, {:integer, 1}}
            _ -> {:error, :no_such_object}
          end
        else
          {:error, :timeout}
        end
      end)

      assert {:ok, sensors} = Base.discover_sensors(@client_opts)

      # Should only have the successful sensor
      assert length(sensors) == 1
      assert hd(sensors).sensor_index == "1"
    end
  end

  describe "identify_device/1" do
    test "identifies Cisco device from sysDescr" do
      system_info = %{
        sys_descr: "Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 15.0(2)SE"
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "Cisco"
      assert device_info.model == "C2960"
    end

    test "identifies Linux server from sysDescr" do
      system_info = %{
        sys_descr: "Linux server 5.4.0-42-generic #46-Ubuntu SMP"
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "Linux"
      # Model uses full sysDescr for Linux devices
      assert device_info.model == "Linux server 5.4.0-42-generic #46-Ubuntu SMP"
    end

    test "identifies Windows server from sysDescr" do
      system_info = %{
        sys_descr: "Windows Server 2019 Standard"
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "Microsoft"
      # Model uses full sysDescr for Windows devices
      assert device_info.model == "Windows Server 2019 Standard"
    end

    test "falls back to unknown for unrecognized devices" do
      system_info = %{
        sys_descr: "Some Unknown Device"
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "Unknown"
      assert device_info.model == "Generic Device"
    end

    test "extracts Cisco model from complex sysDescr" do
      system_info = %{
        sys_descr: "Cisco IOS Software, WS-C3750X Software (cat3k_caa-universalk9-M), Version 15.2(4)E1"
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "Cisco"
      assert device_info.model == "WS-C3750X"
    end

    test "preserves other system_info fields" do
      system_info = %{
        sys_descr: "Linux server",
        sys_name: "webserver01",
        sys_contact: "admin@example.com",
        sys_location: "DC1"
      }

      device_info = Base.identify_device(system_info)

      assert device_info.sys_name == "webserver01"
      assert device_info.sys_contact == "admin@example.com"
      assert device_info.sys_location == "DC1"
    end

    test "identifies Cambium ePMP from sysDescr" do
      system_info = %{
        sys_descr: "Cambium ePMP",
        sys_object_id: "1.3.6.1.4.1.17713.21"
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "Cambium Networks"
      assert device_info.model == "ePMP"
    end

    test "identifies Cambium ePMP 3000 from sysDescr" do
      system_info = %{
        sys_descr: "Cambium ePMP 3000",
        sys_object_id: "1.3.6.1.4.1.17713.21"
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "Cambium Networks"
      assert device_info.model == "ePMP 3000"
    end

    test "identifies Cambium PMP 450 from sysDescr" do
      system_info = %{
        sys_descr: "Cambium PMP 450",
        sys_object_id: "1.3.6.1.4.1.17713.1"
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "Cambium Networks"
      assert device_info.model == "PMP 450"
    end

    test "identifies Cambium ePMP from sysObjectID only (OEM device)" do
      # Verona Networks and other OEMs use Cambium hardware with different sysDescr
      # but the sysObjectID still reveals the product line
      system_info = %{
        sys_descr: "Verona Networks",
        sys_object_id: "1.3.6.1.4.1.17713.21.9.37"
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "Cambium Networks"
      assert device_info.model == "ePMP"
    end

    test "identifies Ubiquiti from sysDescr" do
      system_info = %{
        sys_descr: "Ubiquiti Networks airOS",
        sys_object_id: ""
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "Ubiquiti"
      assert device_info.model == "Wireless"
    end

    test "identifies Ubiquiti AirFiber from sysDescr" do
      system_info = %{
        sys_descr: "AF-5XHD",
        sys_object_id: "1.3.6.1.4.1.41112"
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "Ubiquiti"
      assert device_info.model == "AF-5XHD"
    end

    test "identifies Ubiquiti from sysObjectID" do
      system_info = %{
        sys_descr: "Linux device",
        sys_object_id: "1.3.6.1.4.1.41112.1.2"
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "Ubiquiti"
      assert device_info.model == "Wireless"
    end

    test "identifies MikroTik from sysDescr" do
      system_info = %{
        sys_descr: "RouterOS RB5009UG+S+",
        sys_object_id: ""
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "MikroTik"
      assert device_info.model == "RB5009UG"
    end

    test "identifies MikroTik from sysObjectID" do
      system_info = %{
        sys_descr: "Some Router",
        sys_object_id: "1.3.6.1.4.1.14988.1"
      }

      device_info = Base.identify_device(system_info)

      assert device_info.manufacturer == "MikroTik"
      assert device_info.model == "RouterOS"
    end
  end

  describe "discover_state_sensors/1" do
    test "discovers state sensors from ENTITY-STATE-MIB" do
      # Mock the walk for entity physical class to find modules/power supplies
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          # entPhysicalClass walk - find entities by class
          "1.3.6.1.2.1.47.1.1.1.1.5" ->
            {:ok,
             [
               # powerSupply = 6, fan = 7, sensor = 8, module = 9
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.1001", value: {:integer, 6}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.1002", value: {:integer, 6}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.2001", value: {:integer, 7}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.2002", value: {:integer, 7}}
             ]}

          # entPhysicalDescr walk - descriptions
          "1.3.6.1.2.1.47.1.1.1.1.2" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.1001", value: {:octet_string, "Power Supply 1"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.1002", value: {:octet_string, "Power Supply 2"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.2001", value: {:octet_string, "Fan Tray 1"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.2002", value: {:octet_string, "Fan Tray 2"}}
             ]}

          # ENTITY-STATE-MIB operational status walk
          "1.3.6.1.2.1.131.1.1.1.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.131.1.1.1.1.1001", value: {:integer, 2}},
               %{oid: "1.3.6.1.2.1.131.1.1.1.1.1002", value: {:integer, 2}},
               %{oid: "1.3.6.1.2.1.131.1.1.1.1.2001", value: {:integer, 2}},
               %{oid: "1.3.6.1.2.1.131.1.1.1.1.2002", value: {:integer, 3}}
             ]}

          _ ->
            {:ok, []}
        end
      end)

      stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end)

      assert {:ok, state_sensors} = Base.discover_state_sensors(@client_opts)

      assert length(state_sensors) == 4

      psu1 = Enum.find(state_sensors, &(&1.sensor_index == "1001"))
      assert psu1.sensor_descr == "Power Supply 1"
      assert psu1.entity_type == "power_supply"
      assert psu1.state_value == 2
      assert psu1.status == "ok"

      fan2 = Enum.find(state_sensors, &(&1.sensor_index == "2002"))
      assert fan2.sensor_descr == "Fan Tray 2"
      assert fan2.entity_type == "fan"
      assert fan2.state_value == 3
      assert fan2.status == "warning"
    end

    test "returns empty list when ENTITY-STATE-MIB not supported" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      assert {:ok, state_sensors} = Base.discover_state_sensors(@client_opts)
      assert state_sensors == []
    end

    test "handles walk errors gracefully" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:error, :timeout} end)

      assert {:ok, state_sensors} = Base.discover_state_sensors(@client_opts)
      assert state_sensors == []
    end

    test "filters only power supplies and fans by default" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.47.1.1.1.1.5" ->
            {:ok,
             [
               # chassis = 3, container = 5, powerSupply = 6, fan = 7
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.1", value: {:integer, 3}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.100", value: {:integer, 6}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.200", value: {:integer, 7}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.300", value: {:integer, 5}}
             ]}

          "1.3.6.1.2.1.47.1.1.1.1.2" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.1", value: {:octet_string, "Chassis"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.100", value: {:octet_string, "PSU"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.200", value: {:octet_string, "Fan"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.300", value: {:octet_string, "Container"}}
             ]}

          "1.3.6.1.2.1.131.1.1.1.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.131.1.1.1.1.1", value: {:integer, 2}},
               %{oid: "1.3.6.1.2.1.131.1.1.1.1.100", value: {:integer, 2}},
               %{oid: "1.3.6.1.2.1.131.1.1.1.1.200", value: {:integer, 2}},
               %{oid: "1.3.6.1.2.1.131.1.1.1.1.300", value: {:integer, 2}}
             ]}

          _ ->
            {:ok, []}
        end
      end)

      stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end)

      assert {:ok, state_sensors} = Base.discover_state_sensors(@client_opts)

      # Should only include power supplies and fans (not chassis or container)
      assert length(state_sensors) == 2
      entity_types = Enum.map(state_sensors, & &1.entity_type)
      assert "power_supply" in entity_types
      assert "fan" in entity_types
      refute "chassis" in entity_types
      refute "container" in entity_types
    end

    test "maps ENTITY-STATE-MIB operational status to status correctly" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.47.1.1.1.1.5" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.1", value: {:integer, 6}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.2", value: {:integer, 6}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.3", value: {:integer, 6}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.4", value: {:integer, 6}}
             ]}

          "1.3.6.1.2.1.47.1.1.1.1.2" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.1", value: {:octet_string, "PSU1"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.2", value: {:octet_string, "PSU2"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.3", value: {:octet_string, "PSU3"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.4", value: {:octet_string, "PSU4"}}
             ]}

          "1.3.6.1.2.1.131.1.1.1.1" ->
            {:ok,
             [
               # 1=unknown, 2=enabled(ok), 3=disabled(warning), 4=testing(unknown)
               %{oid: "1.3.6.1.2.1.131.1.1.1.1.1", value: {:integer, 1}},
               %{oid: "1.3.6.1.2.1.131.1.1.1.1.2", value: {:integer, 2}},
               %{oid: "1.3.6.1.2.1.131.1.1.1.1.3", value: {:integer, 3}},
               %{oid: "1.3.6.1.2.1.131.1.1.1.1.4", value: {:integer, 4}}
             ]}

          _ ->
            {:ok, []}
        end
      end)

      stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end)

      assert {:ok, state_sensors} = Base.discover_state_sensors(@client_opts)

      statuses = Map.new(state_sensors, &{&1.sensor_index, &1.status})
      assert statuses["1"] == "unknown"
      assert statuses["2"] == "ok"
      assert statuses["3"] == "warning"
      assert statuses["4"] == "unknown"
    end
  end

  describe "discover_vlans/1" do
    test "discovers VLANs from Q-BRIDGE-MIB" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          # dot1qVlanStaticName - VLAN names
          "1.3.6.1.2.1.17.7.1.4.3.1.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.1", value: {:octet_string, "default"}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.10", value: {:octet_string, "Management"}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.100", value: {:octet_string, "Production"}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.200", value: {:octet_string, "Guest"}}
             ]}

          # dot1qVlanStaticRowStatus - VLAN status
          "1.3.6.1.2.1.17.7.1.4.3.1.5" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.1", value: {:integer, 1}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.10", value: {:integer, 1}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.100", value: {:integer, 1}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.200", value: {:integer, 2}}
             ]}

          _ ->
            {:ok, []}
        end
      end)

      assert {:ok, vlans} = Base.discover_vlans(@client_opts)

      assert length(vlans) == 4

      vlan1 = Enum.find(vlans, &(&1.vlan_id == 1))
      assert vlan1.vlan_name == "default"
      assert vlan1.status == "active"

      vlan100 = Enum.find(vlans, &(&1.vlan_id == 100))
      assert vlan100.vlan_name == "Production"
      assert vlan100.status == "active"

      vlan200 = Enum.find(vlans, &(&1.vlan_id == 200))
      assert vlan200.vlan_name == "Guest"
      assert vlan200.status == "suspended"
    end

    test "returns empty list when Q-BRIDGE-MIB not supported" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      assert {:ok, vlans} = Base.discover_vlans(@client_opts)
      assert vlans == []
    end

    test "handles walk errors gracefully" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:error, :timeout} end)

      assert {:ok, vlans} = Base.discover_vlans(@client_opts)
      assert vlans == []
    end

    test "handles VLANs with missing names" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          # dot1qVlanStaticName - Only some VLANs have names
          "1.3.6.1.2.1.17.7.1.4.3.1.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.1", value: {:octet_string, "default"}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.10", value: {:octet_string, "Management"}}
             ]}

          # dot1qVlanStaticRowStatus - More VLANs exist than have names
          "1.3.6.1.2.1.17.7.1.4.3.1.5" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.1", value: {:integer, 1}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.10", value: {:integer, 1}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.50", value: {:integer, 1}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.100", value: {:integer, 1}}
             ]}

          _ ->
            {:ok, []}
        end
      end)

      assert {:ok, vlans} = Base.discover_vlans(@client_opts)

      # Only 2 VLANs from the name walk (status-only VLANs require fallback)
      assert length(vlans) == 2

      # Named VLANs
      vlan1 = Enum.find(vlans, &(&1.vlan_id == 1))
      assert vlan1.vlan_name == "default"

      vlan10 = Enum.find(vlans, &(&1.vlan_id == 10))
      assert vlan10.vlan_name == "Management"
    end

    test "validates VLAN ID range (1-4094)" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.17.7.1.4.3.1.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.0", value: {:octet_string, "invalid"}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.1", value: {:octet_string, "default"}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.4094", value: {:octet_string, "max"}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.1.4095", value: {:octet_string, "too_high"}}
             ]}

          "1.3.6.1.2.1.17.7.1.4.3.1.5" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.0", value: {:integer, 1}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.1", value: {:integer, 1}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.4094", value: {:integer, 1}},
               %{oid: "1.3.6.1.2.1.17.7.1.4.3.1.5.4095", value: {:integer, 1}}
             ]}

          _ ->
            {:ok, []}
        end
      end)

      assert {:ok, vlans} = Base.discover_vlans(@client_opts)

      vlan_ids = Enum.map(vlans, & &1.vlan_id)

      # Should include valid VLANs
      assert 1 in vlan_ids
      assert 4094 in vlan_ids

      # Should exclude invalid VLANs (0 and 4095+)
      refute 0 in vlan_ids
      refute 4095 in vlan_ids
    end
  end

  describe "discover_ip_addresses/1" do
    test "discovers IPv4 addresses from IP-MIB" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          # ipAdEntIfIndex - maps IP to interface index
          "1.3.6.1.2.1.4.20.1.2" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.4.20.1.2.192.168.1.1", value: {:integer, 1}},
               %{oid: "1.3.6.1.2.1.4.20.1.2.192.168.1.100", value: {:integer, 1}},
               %{oid: "1.3.6.1.2.1.4.20.1.2.10.0.0.1", value: {:integer, 2}}
             ]}

          # ipAdEntNetMask - subnet masks
          "1.3.6.1.2.1.4.20.1.3" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.4.20.1.3.192.168.1.1", value: {:ip_address, {255, 255, 255, 0}}},
               %{oid: "1.3.6.1.2.1.4.20.1.3.192.168.1.100", value: {:ip_address, {255, 255, 255, 0}}},
               %{oid: "1.3.6.1.2.1.4.20.1.3.10.0.0.1", value: {:ip_address, {255, 0, 0, 0}}}
             ]}

          _ ->
            {:ok, []}
        end
      end)

      assert {:ok, ip_addresses} = Base.discover_ip_addresses(@client_opts)

      assert length(ip_addresses) == 3

      ip1 = Enum.find(ip_addresses, &(&1.ip_address == "192.168.1.1"))
      assert ip1.if_index == 1
      assert ip1.subnet_mask == "255.255.255.0"
      assert ip1.prefix_length == 24
      assert ip1.ip_type == "ipv4"

      ip2 = Enum.find(ip_addresses, &(&1.ip_address == "10.0.0.1"))
      assert ip2.if_index == 2
      assert ip2.subnet_mask == "255.0.0.0"
      assert ip2.prefix_length == 8
    end

    test "returns empty list when IP-MIB not supported" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      assert {:ok, ip_addresses} = Base.discover_ip_addresses(@client_opts)
      assert ip_addresses == []
    end

    test "handles walk errors gracefully" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:error, :timeout} end)

      assert {:ok, ip_addresses} = Base.discover_ip_addresses(@client_opts)
      assert ip_addresses == []
    end

    test "handles missing subnet mask gracefully" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.4.20.1.2" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.4.20.1.2.192.168.1.1", value: {:integer, 1}}
             ]}

          "1.3.6.1.2.1.4.20.1.3" ->
            {:ok, []}

          _ ->
            {:ok, []}
        end
      end)

      assert {:ok, ip_addresses} = Base.discover_ip_addresses(@client_opts)

      assert length(ip_addresses) == 1
      ip = hd(ip_addresses)
      assert ip.ip_address == "192.168.1.1"
      assert ip.subnet_mask == nil
      assert ip.prefix_length == nil
    end
  end

  describe "discover_ipv6_addresses/1" do
    test "discovers IPv6 addresses from IPV6-MIB ipv6AddrTable" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          # ipAddressIfIndex - modern RFC 4293 table (empty, fallback to IPV6-MIB)
          "1.3.6.1.2.1.4.34.1.3" ->
            {:ok, []}

          # ipv6AddrPfxLength - indexed by ifIndex.ipv6Address
          # Example: interface 2 with address 2001:db8::1 (16 octets)
          "1.3.6.1.2.1.55.1.8.1.2" ->
            {:ok,
             [
               # ifIndex=2, IPv6=2001:0db8:0000:0000:0000:0000:0000:0001
               %{
                 oid: "1.3.6.1.2.1.55.1.8.1.2.2.32.1.13.184.0.0.0.0.0.0.0.0.0.0.0.1",
                 value: {:integer, 64}
               },
               # ifIndex=3, IPv6=fe80::1
               %{
                 oid: "1.3.6.1.2.1.55.1.8.1.2.3.254.128.0.0.0.0.0.0.0.0.0.0.0.0.0.1",
                 value: {:integer, 64}
               }
             ]}

          _ ->
            {:ok, []}
        end
      end)

      assert {:ok, ip_addresses} = Base.discover_ipv6_addresses(@client_opts)

      assert length(ip_addresses) == 2

      ipv6_1 = Enum.find(ip_addresses, &(&1.if_index == 2))
      assert ipv6_1.ip_address == "2001:db8:0:0:0:0:0:1"
      assert ipv6_1.prefix_length == 64
      assert ipv6_1.ip_type == "ipv6"

      ipv6_2 = Enum.find(ip_addresses, &(&1.if_index == 3))
      assert ipv6_2.ip_address == "fe80:0:0:0:0:0:0:1"
      assert ipv6_2.prefix_length == 64
    end

    test "returns empty list when IPV6-MIB not supported" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      assert {:ok, ip_addresses} = Base.discover_ipv6_addresses(@client_opts)
      assert ip_addresses == []
    end

    test "handles walk errors gracefully" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:error, :timeout} end)

      assert {:ok, ip_addresses} = Base.discover_ipv6_addresses(@client_opts)
      assert ip_addresses == []
    end
  end

  describe "discover_all_ip_addresses/1" do
    test "combines IPv4 and IPv6 addresses" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          # IPv4 from IP-MIB
          "1.3.6.1.2.1.4.20.1.2" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.4.20.1.2.192.168.1.1", value: {:integer, 1}}
             ]}

          "1.3.6.1.2.1.4.20.1.3" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.4.20.1.3.192.168.1.1", value: {:ip_address, {255, 255, 255, 0}}}
             ]}

          # IPv6 from modern ipAddressTable (empty, fallback)
          "1.3.6.1.2.1.4.34.1.3" ->
            {:ok, []}

          # IPv6 from IPV6-MIB
          "1.3.6.1.2.1.55.1.8.1.2" ->
            {:ok,
             [
               %{
                 oid: "1.3.6.1.2.1.55.1.8.1.2.1.254.128.0.0.0.0.0.0.0.0.0.0.0.0.0.1",
                 value: {:integer, 64}
               }
             ]}

          _ ->
            {:ok, []}
        end
      end)

      assert {:ok, all_addresses} = Base.discover_all_ip_addresses(@client_opts)

      assert length(all_addresses) == 2

      ipv4 = Enum.find(all_addresses, &(&1.ip_type == "ipv4"))
      assert ipv4.ip_address == "192.168.1.1"
      assert ipv4.prefix_length == 24

      ipv6 = Enum.find(all_addresses, &(&1.ip_type == "ipv6"))
      assert ipv6.ip_address == "fe80:0:0:0:0:0:0:1"
      assert ipv6.prefix_length == 64
    end

    test "returns empty list when no addresses found" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      assert {:ok, all_addresses} = Base.discover_all_ip_addresses(@client_opts)
      assert all_addresses == []
    end
  end

  describe "discover_memory_pools/1" do
    test "discovers memory pools from HOST-RESOURCES-MIB" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          # hrStorageType - type references
          "1.3.6.1.2.1.25.2.3.1.2" ->
            {:ok,
             [
               # RAM: 1.3.6.1.2.1.25.2.1.2
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.1", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 2]}},
               # Virtual Memory: 1.3.6.1.2.1.25.2.1.3
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.3", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 3]}},
               # Fixed Disk: 1.3.6.1.2.1.25.2.1.4 (filtered out for memory)
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.31", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 4]}}
             ]}

          # hrStorageDescr - descriptions
          "1.3.6.1.2.1.25.2.3.1.3" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.25.2.3.1.3.1", value: {:octet_string, "Physical memory"}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.3.3", value: {:octet_string, "Virtual memory"}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.3.31", value: {:octet_string, "/dev/sda1"}}
             ]}

          # hrStorageAllocationUnits
          "1.3.6.1.2.1.25.2.3.1.4" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.25.2.3.1.4.1", value: {:integer, 1024}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.4.3", value: {:integer, 4096}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.4.31", value: {:integer, 4096}}
             ]}

          # hrStorageSize - total units
          "1.3.6.1.2.1.25.2.3.1.5" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.25.2.3.1.5.1", value: {:integer, 8_388_608}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.5.3", value: {:integer, 16_777_216}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.5.31", value: {:integer, 244_140_625}}
             ]}

          # hrStorageUsed - used units
          "1.3.6.1.2.1.25.2.3.1.6" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.25.2.3.1.6.1", value: {:integer, 4_194_304}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.6.3", value: {:integer, 8_388_608}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.6.31", value: {:integer, 122_070_312}}
             ]}

          _ ->
            {:ok, []}
        end
      end)

      assert {:ok, memory_pools} = Base.discover_memory_pools(@client_opts)

      # Should only return RAM and Virtual Memory, not Fixed Disk
      assert length(memory_pools) == 2

      ram = Enum.find(memory_pools, &(&1.pool_index == "1"))
      assert ram.pool_name == "Physical memory"
      assert ram.pool_type == "ram"
      assert ram.total_bytes == 8_388_608 * 1024
      assert ram.used_bytes == 4_194_304 * 1024

      virtual = Enum.find(memory_pools, &(&1.pool_index == "3"))
      assert virtual.pool_name == "Virtual memory"
      assert virtual.pool_type == "swap"
      assert virtual.total_bytes == 16_777_216 * 4096
      assert virtual.used_bytes == 8_388_608 * 4096
    end

    test "returns empty list when HOST-RESOURCES-MIB not supported" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      assert {:ok, memory_pools} = Base.discover_memory_pools(@client_opts)
      assert memory_pools == []
    end

    test "handles walk errors gracefully" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:error, :timeout} end)

      assert {:ok, memory_pools} = Base.discover_memory_pools(@client_opts)
      assert memory_pools == []
    end
  end

  describe "discover_storage/1" do
    test "discovers storage from HOST-RESOURCES-MIB" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          # hrStorageType - type references
          "1.3.6.1.2.1.25.2.3.1.2" ->
            {:ok,
             [
               # RAM (filtered out for storage)
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.1", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 2]}},
               # Fixed Disk
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.31", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 4]}},
               # Removable Disk
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.32", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 5]}},
               # Network Disk
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.33", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 10]}}
             ]}

          # hrStorageDescr
          "1.3.6.1.2.1.25.2.3.1.3" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.25.2.3.1.3.1", value: {:octet_string, "Physical memory"}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.3.31", value: {:octet_string, "/"}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.3.32", value: {:octet_string, "/mnt/usb"}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.3.33", value: {:octet_string, "/mnt/nfs"}}
             ]}

          # hrStorageAllocationUnits
          "1.3.6.1.2.1.25.2.3.1.4" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.25.2.3.1.4.1", value: {:integer, 1024}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.4.31", value: {:integer, 4096}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.4.32", value: {:integer, 4096}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.4.33", value: {:integer, 4096}}
             ]}

          # hrStorageSize
          "1.3.6.1.2.1.25.2.3.1.5" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.25.2.3.1.5.1", value: {:integer, 8_388_608}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.5.31", value: {:integer, 244_140_625}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.5.32", value: {:integer, 7_812_500}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.5.33", value: {:integer, 976_562_500}}
             ]}

          # hrStorageUsed
          "1.3.6.1.2.1.25.2.3.1.6" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.25.2.3.1.6.1", value: {:integer, 4_194_304}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.6.31", value: {:integer, 122_070_312}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.6.32", value: {:integer, 3_906_250}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.6.33", value: {:integer, 488_281_250}}
             ]}

          _ ->
            {:ok, []}
        end
      end)

      assert {:ok, storage} = Base.discover_storage(@client_opts)

      # Should only return Fixed Disk, Removable Disk, Network Disk (not RAM)
      assert length(storage) == 3

      root = Enum.find(storage, &(&1.storage_index == 31))
      assert root.description == "/"
      assert root.storage_type == "fixed_disk"
      assert root.allocation_units == 4096
      assert root.total_bytes == 244_140_625 * 4096
      assert root.used_bytes == 122_070_312 * 4096

      usb = Enum.find(storage, &(&1.storage_index == 32))
      assert usb.description == "/mnt/usb"
      assert usb.storage_type == "removable_disk"

      nfs = Enum.find(storage, &(&1.storage_index == 33))
      assert nfs.description == "/mnt/nfs"
      assert nfs.storage_type == "network_disk"
    end

    test "returns empty list when HOST-RESOURCES-MIB not supported" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      assert {:ok, storage} = Base.discover_storage(@client_opts)
      assert storage == []
    end

    test "handles walk errors gracefully" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:error, :timeout} end)

      assert {:ok, storage} = Base.discover_storage(@client_opts)
      assert storage == []
    end

    test "maps all storage type OIDs correctly" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.25.2.3.1.2" ->
            {:ok,
             [
               # hrStorageType values 1-10 (other, ram, virtual_memory, fixed_disk, etc.)
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.1", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 1]}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.2", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 2]}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.3", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 3]}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.4", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 4]}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.5", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 5]}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.6", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 6]}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.7", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 7]}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.8", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 8]}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.9", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 9]}},
               %{oid: "1.3.6.1.2.1.25.2.3.1.2.10", value: {:object_identifier, [1, 3, 6, 1, 2, 1, 25, 2, 1, 10]}}
             ]}

          "1.3.6.1.2.1.25.2.3.1.3" ->
            {:ok,
             Enum.map(1..10, fn i ->
               %{oid: "1.3.6.1.2.1.25.2.3.1.3.#{i}", value: {:octet_string, "Storage #{i}"}}
             end)}

          "1.3.6.1.2.1.25.2.3.1.4" ->
            {:ok,
             Enum.map(1..10, fn i ->
               %{oid: "1.3.6.1.2.1.25.2.3.1.4.#{i}", value: {:integer, 4096}}
             end)}

          "1.3.6.1.2.1.25.2.3.1.5" ->
            {:ok,
             Enum.map(1..10, fn i ->
               %{oid: "1.3.6.1.2.1.25.2.3.1.5.#{i}", value: {:integer, 1000}}
             end)}

          "1.3.6.1.2.1.25.2.3.1.6" ->
            {:ok,
             Enum.map(1..10, fn i ->
               %{oid: "1.3.6.1.2.1.25.2.3.1.6.#{i}", value: {:integer, 500}}
             end)}

          _ ->
            {:ok, []}
        end
      end)

      assert {:ok, storage} = Base.discover_storage(@client_opts)

      # Only disk-type storage (indices 4-10, not RAM/virtual memory)
      assert length(storage) == 7

      expected_types = [
        {4, "fixed_disk"},
        {5, "removable_disk"},
        {6, "floppy_disk"},
        {7, "compact_disc"},
        {8, "ram_disk"},
        {9, "flash_memory"},
        {10, "network_disk"}
      ]

      for {idx, expected_type} <- expected_types do
        entry = Enum.find(storage, &(&1.storage_index == idx))
        assert entry.storage_type == expected_type, "Index #{idx} should have type #{expected_type}"
      end
    end
  end

  describe "discover_physical_entities/1" do
    test "discovers physical entities from ENTITY-MIB" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          # entPhysicalDescr
          "1.3.6.1.2.1.47.1.1.1.1.2" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.1", value: {:octet_string, "Cisco 2960-X Chassis"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.2", value: {:octet_string, "Power Supply 1"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.3", value: {:octet_string, "Fan Tray 1"}}
             ]}

          # entPhysicalContainedIn
          "1.3.6.1.2.1.47.1.1.1.1.4" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.4.1", value: {:integer, 0}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.4.2", value: {:integer, 1}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.4.3", value: {:integer, 1}}
             ]}

          # entPhysicalClass
          "1.3.6.1.2.1.47.1.1.1.1.5" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.1", value: {:integer, 3}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.2", value: {:integer, 6}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.3", value: {:integer, 7}}
             ]}

          # entPhysicalSerialNum
          "1.3.6.1.2.1.47.1.1.1.1.11" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.11.1", value: {:octet_string, "FCW1234ABC"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.11.2", value: {:octet_string, "PSU-001"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.11.3", value: {:octet_string, ""}}
             ]}

          # entPhysicalModelName
          "1.3.6.1.2.1.47.1.1.1.1.13" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.13.1", value: {:octet_string, "WS-C2960X-24TS-L"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.13.2", value: {:octet_string, "PWR-C1-350WAC"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.13.3", value: {:octet_string, "FAN-MOD-4HS"}}
             ]}

          # entPhysicalMfgName
          "1.3.6.1.2.1.47.1.1.1.1.12" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.12.1", value: {:octet_string, "Cisco"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.12.2", value: {:octet_string, "Cisco"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.12.3", value: {:octet_string, "Cisco"}}
             ]}

          # entPhysicalHardwareRev
          "1.3.6.1.2.1.47.1.1.1.1.8" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.8.1", value: {:octet_string, "V01"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.8.2", value: {:octet_string, "A0"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.8.3", value: {:octet_string, ""}}
             ]}

          # entPhysicalFirmwareRev
          "1.3.6.1.2.1.47.1.1.1.1.9" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.9.1", value: {:octet_string, "15.2(4)E7"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.9.2", value: {:octet_string, ""}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.9.3", value: {:octet_string, ""}}
             ]}

          _ ->
            {:ok, []}
        end
      end)

      assert {:ok, entities} = Base.discover_physical_entities(@client_opts)

      assert length(entities) == 3

      chassis = Enum.find(entities, &(&1.entity_index == 1))
      assert chassis.description == "Cisco 2960-X Chassis"
      assert chassis.entity_class == "chassis"
      assert chassis.serial_number == "FCW1234ABC"
      assert chassis.model_name == "WS-C2960X-24TS-L"
      assert chassis.manufacturer_name == "Cisco"
      assert chassis.hardware_revision == "V01"
      assert chassis.firmware_revision == "15.2(4)E7"
      assert chassis.contained_in == 0

      psu = Enum.find(entities, &(&1.entity_index == 2))
      assert psu.description == "Power Supply 1"
      assert psu.entity_class == "power"
      assert psu.contained_in == 1

      fan = Enum.find(entities, &(&1.entity_index == 3))
      assert fan.description == "Fan Tray 1"
      assert fan.entity_class == "fan"
      assert fan.contained_in == 1
    end

    test "returns empty list when ENTITY-MIB not supported" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      assert {:ok, entities} = Base.discover_physical_entities(@client_opts)
      assert entities == []
    end

    test "handles walk errors gracefully" do
      stub(SnmpMock, :walk, fn _, _, _ -> {:error, :timeout} end)

      assert {:ok, entities} = Base.discover_physical_entities(@client_opts)
      assert entities == []
    end

    test "maps entity class integers to strings" do
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.47.1.1.1.1.2" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.1", value: {:octet_string, "Other"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.2", value: {:octet_string, "Unknown"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.3", value: {:octet_string, "Chassis"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.4", value: {:octet_string, "Backplane"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.5", value: {:octet_string, "Container"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.6", value: {:octet_string, "PSU"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.7", value: {:octet_string, "Fan"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.8", value: {:octet_string, "Sensor"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.9", value: {:octet_string, "Module"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.10", value: {:octet_string, "Port"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.11", value: {:octet_string, "Stack"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.12", value: {:octet_string, "CPU"}},
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.2.13", value: {:octet_string, "Memory"}}
             ]}

          "1.3.6.1.2.1.47.1.1.1.1.4" ->
            {:ok,
             Enum.map(1..13, fn i ->
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.4.#{i}", value: {:integer, 0}}
             end)}

          "1.3.6.1.2.1.47.1.1.1.1.5" ->
            {:ok,
             Enum.map(1..13, fn i ->
               %{oid: "1.3.6.1.2.1.47.1.1.1.1.5.#{i}", value: {:integer, i}}
             end)}

          _ ->
            {:ok, []}
        end
      end)

      assert {:ok, entities} = Base.discover_physical_entities(@client_opts)

      assert length(entities) == 13

      expected_classes = [
        "other",
        "unknown",
        "chassis",
        "backplane",
        "container",
        "power",
        "fan",
        "sensor",
        "module",
        "port",
        "stack",
        "cpu",
        "memory"
      ]

      for {expected_class, idx} <- Enum.with_index(expected_classes, 1) do
        entity = Enum.find(entities, &(&1.entity_index == idx))
        assert entity.entity_class == expected_class, "Entity #{idx} should have class #{expected_class}"
      end
    end
  end

  describe "discover_processors/1" do
    test "discovers processors from HOST-RESOURCES-MIB" do
      # Mock hrProcessorLoad walk
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.25.3.3.1.2", _ ->
        {:ok,
         [
           %{oid: "1.3.6.1.2.1.25.3.3.1.2.768", value: {:integer, 25}},
           %{oid: "1.3.6.1.2.1.25.3.3.1.2.769", value: {:integer, 45}}
         ]}
      end)

      # Mock hrDeviceDescr walk for processor descriptions
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.25.3.2.1.3", _ ->
        {:ok,
         [
           %{oid: "1.3.6.1.2.1.25.3.2.1.3.768", value: {:octet_string, "Intel Core i7-9700 @ 3.00GHz"}},
           %{oid: "1.3.6.1.2.1.25.3.2.1.3.769", value: {:octet_string, "Intel Core i7-9700 @ 3.00GHz"}}
         ]}
      end)

      assert {:ok, processors} = Base.discover_processors(@client_opts)

      assert length(processors) == 2

      cpu1 = Enum.find(processors, &(&1.processor_index == "hr_768"))
      assert cpu1.description == "Intel Core i7-9700 @ 3.00GHz"
      assert cpu1.processor_type == "hr_processor"
      assert cpu1.load_percent == 25.0

      cpu2 = Enum.find(processors, &(&1.processor_index == "hr_769"))
      assert cpu2.description == "Intel Core i7-9700 @ 3.00GHz"
      assert cpu2.processor_type == "hr_processor"
      assert cpu2.load_percent == 45.0
    end

    test "falls back to CISCO-PROCESS-MIB when HOST-RESOURCES-MIB returns empty" do
      # HR-MIB returns empty
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.25.3.3.1.2", _ ->
        {:ok, []}
      end)

      # Cisco CPU walk returns data
      expect(SnmpMock, :walk, fn _, "1.3.6.1.4.1.9.9.109.1.1.1.1.5", _ ->
        {:ok,
         [
           %{oid: "1.3.6.1.4.1.9.9.109.1.1.1.1.5.1", value: {:integer, 15}},
           %{oid: "1.3.6.1.4.1.9.9.109.1.1.1.1.5.2", value: {:integer, 22}}
         ]}
      end)

      assert {:ok, processors} = Base.discover_processors(@client_opts)

      assert length(processors) == 2

      cpu1 = Enum.find(processors, &(&1.processor_index == "cisco_1"))
      assert cpu1.description == "Cisco CPU 1"
      assert cpu1.processor_type == "cisco_cpu"
      assert cpu1.load_percent == 15.0

      cpu2 = Enum.find(processors, &(&1.processor_index == "cisco_2"))
      assert cpu2.description == "Cisco CPU 2"
      assert cpu2.processor_type == "cisco_cpu"
      assert cpu2.load_percent == 22.0
    end

    test "falls back to UCD-SNMP-MIB when other MIBs return empty" do
      # HR-MIB returns empty
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.25.3.3.1.2", _ ->
        {:ok, []}
      end)

      # Cisco CPU returns empty
      expect(SnmpMock, :walk, fn _, "1.3.6.1.4.1.9.9.109.1.1.1.1.5", _ ->
        {:ok, []}
      end)

      # UCD-SNMP-MIB CPU stats
      expect(SnmpMock, :get, 3, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.2021.11.9.0" -> {:ok, {:integer, 10}}
          "1.3.6.1.4.1.2021.11.10.0" -> {:ok, {:integer, 5}}
          "1.3.6.1.4.1.2021.11.11.0" -> {:ok, {:integer, 85}}
        end
      end)

      assert {:ok, processors} = Base.discover_processors(@client_opts)

      assert length(processors) == 1

      cpu = hd(processors)
      assert cpu.processor_index == "ucd_0"
      assert cpu.description == "System CPU"
      assert cpu.processor_type == "ucd_cpu"
      assert cpu.load_percent == 15.0
      assert cpu.metadata.user_percent == 10
      assert cpu.metadata.system_percent == 5
    end

    test "returns empty list when no CPU MIBs are supported" do
      # All walks return empty
      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)
      stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end)

      assert {:ok, processors} = Base.discover_processors(@client_opts)
      assert processors == []
    end

    test "handles HOST-RESOURCES-MIB walk errors gracefully" do
      # HR-MIB walk fails
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.25.3.3.1.2", _ ->
        {:error, :timeout}
      end)

      # Falls back to Cisco
      expect(SnmpMock, :walk, fn _, "1.3.6.1.4.1.9.9.109.1.1.1.1.5", _ ->
        {:ok,
         [
           %{oid: "1.3.6.1.4.1.9.9.109.1.1.1.1.5.1", value: {:integer, 30}}
         ]}
      end)

      assert {:ok, processors} = Base.discover_processors(@client_opts)
      assert length(processors) == 1
      assert hd(processors).processor_type == "cisco_cpu"
    end

    test "uses default description when hrDeviceDescr is missing" do
      # HR processor load
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.25.3.3.1.2", _ ->
        {:ok,
         [
           %{oid: "1.3.6.1.2.1.25.3.3.1.2.768", value: {:integer, 50}}
         ]}
      end)

      # HR device description returns empty
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.25.3.2.1.3", _ ->
        {:ok, []}
      end)

      assert {:ok, processors} = Base.discover_processors(@client_opts)

      assert length(processors) == 1
      cpu = hd(processors)
      assert cpu.description == "CPU 768"
      assert cpu.load_percent == 50.0
    end
  end
end
