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

  import Mox

  alias Towerops.Snmp.Profiles.Dynamic
  alias Towerops.Snmp.SnmpMock

  setup :verify_on_exit!

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

  # A minimal profile map with no device_oids or sensor_oids
  defp base_profile(overrides \\ %{}) do
    Map.merge(
      %{
        name: "test_device",
        vendor: "TestVendor",
        type: "router",
        device_oids: %{},
        sensor_oids: [],
        table_sensor_oids: [],
        processor_oids: [],
        count_sensor_oids: [],
        state_sensor_oids: []
      },
      overrides
    )
  end

  defp base_system_info(overrides \\ %{}) do
    Map.merge(
      %{
        sys_descr: "Test Device v1.0",
        sys_name: "test-device-01",
        sys_object_id: "1.3.6.1.4.1.99999.1"
      },
      overrides
    )
  end

  describe "identify_device/3" do
    test "returns basic device info when profile has no device_oids" do
      profile = base_profile()
      system_info = base_system_info()

      # No SNMP calls needed when device_oids is empty, but
      # Base.discover_sensors is not called by identify_device so no walks needed.
      # However, vendor detection calls may happen. Stub everything to fail gracefully.
      stub(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end)
      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      result = Dynamic.identify_device(profile, @client_opts, system_info)

      assert result.manufacturer == "TestVendor"
      assert result.sys_descr == "Test Device v1.0"
      # model falls through to profile_based_model_fallback -> "Router Device"
      # since vendor detection returns nil and no regex
      assert result.model == "Router Device"
    end

    test "uses device_oids to fetch hardware, firmware, and serial from SNMP" do
      profile =
        base_profile(%{
          device_oids: %{
            hardware: "1.3.6.1.4.1.99999.1.1.0",
            firmware_version: "1.3.6.1.4.1.99999.1.2.0",
            serial_number: "1.3.6.1.4.1.99999.1.3.0"
          }
        })

      system_info = base_system_info()

      stub(SnmpMock, :get, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.1.1.0" -> {:ok, {:octet_string, "Model X100"}}
          "1.3.6.1.4.1.99999.1.2.0" -> {:ok, {:octet_string, "2.5.1"}}
          "1.3.6.1.4.1.99999.1.3.0" -> {:ok, {:octet_string, "SN-ABC123"}}
          _ -> {:error, :no_such_object}
        end
      end)

      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      result = Dynamic.identify_device(profile, @client_opts, system_info)

      assert result.manufacturer == "TestVendor"
      assert result.model == "Model X100"
      assert result.firmware_version == "2.5.1"
      assert result.serial_number == "SN-ABC123"
    end

    test "falls back to sysDescr when all model detection methods fail" do
      # Profile with no type to prevent profile_based_model_fallback
      profile = base_profile(%{type: nil, vendor: nil})
      system_info = base_system_info(%{sys_descr: "Generic Router 3000"})

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

      result = Dynamic.identify_device(profile, @client_opts, system_info)

      assert result.model == "Generic Router 3000"
    end

    test "extracts hardware from sysDescr using hardware_regex" do
      profile =
        base_profile(%{
          hardware_regex: ~r/^(?<hardware>[\w-]+)\s+Software/
        })

      system_info = base_system_info(%{sys_descr: "CX-5000 Software Version 4.2"})

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

      result = Dynamic.identify_device(profile, @client_opts, system_info)

      assert result.model == "CX-5000"
    end

    test "hardware_regex returns nil when no match" do
      profile =
        base_profile(%{
          hardware_regex: ~r/^(?<hardware>RouterOS\s+\w+)/
        })

      system_info = base_system_info(%{sys_descr: "Totally different device"})

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

      result = Dynamic.identify_device(profile, @client_opts, system_info)

      # Falls through to profile_based_model_fallback -> "Router Device"
      assert result.model == "Router Device"
    end

    test "profile_based_model_fallback uses profile type" do
      profile = base_profile(%{type: "wireless", vendor: "Acme"})
      system_info = base_system_info()

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

      result = Dynamic.identify_device(profile, @client_opts, system_info)

      assert result.model == "Wireless Device"
      assert result.manufacturer == "Acme"
    end

    test "preserves system_info fields in result" do
      profile = base_profile()
      system_info = base_system_info(%{sys_name: "core-router", sys_contact: "noc@example.com"})

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

      result = Dynamic.identify_device(profile, @client_opts, system_info)

      assert result.sys_name == "core-router"
      assert result.sys_contact == "noc@example.com"
    end

    test "sets latitude and longitude from device_oids" do
      profile =
        base_profile(%{
          device_oids: %{
            latitude: "1.3.6.1.4.1.99999.1.10.0",
            longitude: "1.3.6.1.4.1.99999.1.11.0"
          }
        })

      system_info = base_system_info()

      stub(SnmpMock, :get, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.1.10.0" -> {:ok, {:octet_string, "40.7128"}}
          "1.3.6.1.4.1.99999.1.11.0" -> {:ok, {:octet_string, "-74.0060"}}
          _ -> {:error, :no_such_object}
        end
      end)

      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      result = Dynamic.identify_device(profile, @client_opts, system_info)

      assert result.latitude == "40.7128"
      assert result.longitude == "-74.0060"
    end
  end

  describe "identify_device/3 firmware formatting" do
    test "uses raw firmware_version from device_oids when available" do
      # When firmware_version is fetched via device_oids, the raw value is used
      # directly (short-circuits before format_firmware_version is called)
      profile =
        base_profile(%{
          vendor: "MikroTik",
          device_oids: %{
            firmware_version: "1.3.6.1.4.1.99999.1.2.0"
          }
        })

      system_info = base_system_info()

      stub(SnmpMock, :get, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.1.2.0" -> {:ok, {:octet_string, "7.15.3"}}
          _ -> {:error, :no_such_object}
        end
      end)

      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      result = Dynamic.identify_device(profile, @client_opts, system_info)

      # Raw value is used - format_firmware_version is not reached
      assert result.firmware_version == "7.15.3"
    end

    test "firmware_version is nil when no device_oid and no vendor detection" do
      profile = base_profile(%{vendor: "TestVendor"})
      system_info = base_system_info()

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

      result = Dynamic.identify_device(profile, @client_opts, system_info)

      assert result.firmware_version == nil
    end

    test "returns raw firmware for generic vendor" do
      profile =
        base_profile(%{
          device_oids: %{
            firmware_version: "1.3.6.1.4.1.99999.1.2.0"
          }
        })

      system_info = base_system_info()

      stub(SnmpMock, :get, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.1.2.0" -> {:ok, {:octet_string, "4.2.1-build100"}}
          _ -> {:error, :no_such_object}
        end
      end)

      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      result = Dynamic.identify_device(profile, @client_opts, system_info)

      assert result.firmware_version == "4.2.1-build100"
    end

    test "MikroTik device_oids with firmware and license populates device_data" do
      profile =
        base_profile(%{
          vendor: "MikroTik",
          device_oids: %{
            firmware_version: "1.3.6.1.4.1.99999.1.2.0",
            license_version: "1.3.6.1.4.1.99999.1.4.0"
          }
        })

      system_info = base_system_info()

      stub(SnmpMock, :get, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.1.2.0" -> {:ok, {:octet_string, "7.15.3"}}
          "1.3.6.1.4.1.99999.1.4.0" -> {:ok, {:octet_string, "6"}}
          _ -> {:error, :no_such_object}
        end
      end)

      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      result = Dynamic.identify_device(profile, @client_opts, system_info)

      # Raw firmware_version from device_oids is used directly
      assert result.firmware_version == "7.15.3"
      # License version is also in the device_data merge
      assert result.license_version == "6"
    end

    test "Ubiquiti device with raw firmware version" do
      profile =
        base_profile(%{
          vendor: "Ubiquiti AirOS",
          device_oids: %{
            firmware_version: "1.3.6.1.4.1.99999.1.2.0"
          }
        })

      system_info = base_system_info()

      stub(SnmpMock, :get, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.1.2.0" -> {:ok, {:octet_string, "XW.v8.7.21"}}
          _ -> {:error, :no_such_object}
        end
      end)

      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      result = Dynamic.identify_device(profile, @client_opts, system_info)

      # Raw value from device_oids is used directly
      assert result.firmware_version == "XW.v8.7.21"
    end
  end

  describe "discover_sensors/2" do
    test "returns base sensors when profile has no sensor definitions" do
      profile = base_profile()

      # Base.discover_sensors will walk ENTITY-SENSOR-MIB
      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

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

    test "discovers scalar sensors from profile sensor_oids" do
      profile =
        base_profile(%{
          sensor_oids: [
            %{
              mib_name: "1.3.6.1.4.1.99999.2.1.0",
              sensor_type: "temperature",
              sensor_descr: "CPU Temp",
              sensor_unit: "C",
              sensor_divisor: 10
            },
            %{
              mib_name: "1.3.6.1.4.1.99999.2.2.0",
              sensor_type: "voltage",
              sensor_descr: "Main Power",
              sensor_unit: "V",
              sensor_divisor: 1000
            }
          ]
        })

      stub(SnmpMock, :get, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.2.1.0" -> {:ok, {:integer, 450}}
          "1.3.6.1.4.1.99999.2.2.0" -> {:ok, {:integer, 12_050}}
          _ -> {:error, :no_such_object}
        end
      end)

      # Base sensor and table walks return empty
      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

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

      temp = Enum.find(sensors, &(&1.sensor_type == "temperature"))
      assert temp.sensor_descr == "CPU Temp"
      assert temp.sensor_unit == "C"
      assert temp.sensor_divisor == 10
      assert temp.last_value == 450

      voltage = Enum.find(sensors, &(&1.sensor_type == "voltage"))
      assert voltage.sensor_descr == "Main Power"
      assert voltage.sensor_unit == "V"
    end

    test "skips scalar sensors with zero or non-integer values" do
      profile =
        base_profile(%{
          sensor_oids: [
            %{
              mib_name: "1.3.6.1.4.1.99999.2.1.0",
              sensor_type: "temperature",
              sensor_descr: "CPU Temp",
              sensor_unit: "C"
            },
            %{
              mib_name: "1.3.6.1.4.1.99999.2.2.0",
              sensor_type: "voltage",
              sensor_descr: "Bad Sensor"
            }
          ]
        })

      stub(SnmpMock, :get, fn _, oid, _ ->
        case oid do
          # This one returns 0 - should be skipped (value > 0 check)
          "1.3.6.1.4.1.99999.2.1.0" -> {:ok, {:integer, 0}}
          # This one errors - should be skipped
          "1.3.6.1.4.1.99999.2.2.0" -> {:error, :no_such_object}
          _ -> {:error, :no_such_object}
        end
      end)

      stub(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

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

      # Both should be filtered out
      assert Enum.empty?(sensors)
    end

    test "discovers table-based sensors via walk" do
      profile =
        base_profile(%{
          table_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.3.1",
              sensor_type: "temperature",
              sensor_descr: "Temp Sensor",
              sensor_unit: "C",
              sensor_divisor: 10,
              descr_oid: nil
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.3.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.4.1.99999.3.1.1", value: {:integer, 450}},
               %{oid: "1.3.6.1.4.1.99999.3.1.2", value: {:integer, 520}}
             ]}

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

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

      temp_sensors = Enum.filter(sensors, &(&1.sensor_type == "temperature"))
      assert length(temp_sensors) == 2

      first = Enum.find(temp_sensors, &(&1.sensor_index == "temperature_1"))
      assert first.last_value == 450
      assert first.sensor_unit == "C"
      assert first.sensor_divisor == 10
      assert first.sensor_descr == "Temp sensor"

      second = Enum.find(temp_sensors, &(&1.sensor_index == "temperature_2"))
      assert second.last_value == 520
      assert second.sensor_descr == "Temp sensor 2"
    end

    test "discovers table sensors with description OID walk" do
      profile =
        base_profile(%{
          table_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.3.1",
              descr_oid: "1.3.6.1.4.1.99999.3.2",
              sensor_type: "temperature",
              sensor_descr: "Temp Sensor",
              sensor_unit: "C",
              sensor_divisor: 1
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.3.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.4.1.99999.3.1.1", value: {:integer, 42}},
               %{oid: "1.3.6.1.4.1.99999.3.1.2", value: {:integer, 55}}
             ]}

          "1.3.6.1.4.1.99999.3.2" ->
            {:ok,
             [
               %{oid: "1.3.6.1.4.1.99999.3.2.1", value: {:octet_string, "Board Temp"}},
               %{oid: "1.3.6.1.4.1.99999.3.2.2", value: {:octet_string, "CPU Temp"}}
             ]}

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

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

      temp_sensors = Enum.filter(sensors, &(&1.sensor_type == "temperature"))
      assert length(temp_sensors) == 2

      # Descriptions come from the descr_oid walk results
      descrs = temp_sensors |> Enum.map(& &1.sensor_descr) |> Enum.sort()
      assert descrs == ["Board Temp", "CPU Temp"]
    end

    test "discovers state sensors with state mapping" do
      profile =
        base_profile(%{
          state_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.4.1",
              oid_name: "gps_status",
              sensor_type: "state",
              sensor_descr: "GPS Status",
              states: %{0 => "No Fix", 1 => "2D Fix", 2 => "3D Fix"}
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.4.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.4.1.99999.4.1.0", value: {:integer, 2}}
             ]}

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

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

      state_sensors = Enum.filter(sensors, &(&1.sensor_type == "state"))
      assert length(state_sensors) == 1

      gps = hd(state_sensors)
      assert gps.sensor_index == "gps_status_1"
      assert gps.state_descr == "3D Fix"
      assert gps.last_value == 2
      assert gps.metadata["states"]["2"] == "3D Fix"
      assert gps.metadata["states"]["0"] == "No Fix"
    end

    test "state sensor uses fallback description for unknown state values" do
      profile =
        base_profile(%{
          state_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.4.1",
              oid_name: "link_status",
              sensor_type: "state",
              sensor_descr: "Link Status",
              states: %{0 => "Down", 1 => "Up"}
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.4.1" ->
            {:ok,
             [
               # State 99 is not in the states map
               %{oid: "1.3.6.1.4.1.99999.4.1.0", value: {:integer, 99}}
             ]}

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

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

      state_sensor = Enum.find(sensors, &(&1.sensor_type == "state"))
      assert state_sensor.state_descr == "State 99"
    end

    test "skips non-integer values in table walk results" do
      profile =
        base_profile(%{
          table_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.3.1",
              sensor_type: "temperature",
              sensor_descr: "Temp",
              sensor_unit: "C",
              sensor_divisor: 1,
              descr_oid: nil
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.3.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.4.1.99999.3.1.1", value: {:integer, 42}},
               # String value should be filtered out
               %{oid: "1.3.6.1.4.1.99999.3.1.2", value: {:octet_string, "not a number"}}
             ]}

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

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

      temp_sensors = Enum.filter(sensors, &(&1.sensor_type == "temperature"))
      assert length(temp_sensors) == 1
      assert hd(temp_sensors).last_value == 42
    end

    test "handles empty walk results for table sensors" do
      profile =
        base_profile(%{
          table_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.3.1",
              sensor_type: "temperature",
              sensor_descr: "Temp",
              sensor_unit: "C",
              sensor_divisor: 1,
              descr_oid: nil
            }
          ]
        })

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

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

    test "handles walk errors for table sensors" do
      profile =
        base_profile(%{
          table_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.3.1",
              sensor_type: "temperature",
              sensor_descr: "Temp",
              sensor_unit: "C",
              sensor_divisor: 1,
              descr_oid: nil
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, _, _ -> {:error, :timeout} end)

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

    test "deduplicates sensors by OID, preferring non-MIB descriptions" do
      # This tests deduplicate_by_oid/1 through discover_sensors.
      # We need two sources that produce sensors with the same OID:
      # table_sensor_oids and the base ENTITY-SENSOR-MIB.
      # We'll use table_sensor_oids for a sensor with a proper name,
      # and count_sensor_oids for a duplicate with MIB symbolic name.
      profile =
        base_profile(%{
          table_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.3.1",
              sensor_type: "temperature",
              sensor_descr: "Board Temperature",
              sensor_unit: "C",
              sensor_divisor: 1,
              descr_oid: nil
            }
          ],
          count_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.3.1",
              sensor_type: "count",
              sensor_descr: "VENDOR-MIB::sensorTemp",
              sensor_unit: "",
              sensor_divisor: 1,
              descr_oid: nil
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.3.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.4.1.99999.3.1.1", value: {:integer, 42}}
             ]}

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

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

      # Should be deduplicated - one sensor per OID
      sensors_at_oid =
        Enum.filter(sensors, &(&1.sensor_oid == "1.3.6.1.4.1.99999.3.1.1"))

      assert length(sensors_at_oid) == 1

      # Should prefer the one without MIB symbolic name in description
      sensor = hd(sensors_at_oid)
      refute String.contains?(sensor.sensor_descr, "::")
    end

    test "deduplicates sensors with leading-dot OID differences" do
      # Agent (gosnmp) returns OIDs with leading dots from walks, but scalar
      # sensor_oids use the MIB name without leading dots. Both produce sensors
      # for the same OID but with different formats - deduplication must match them.
      profile =
        base_profile(%{
          sensor_oids: [
            %{
              mib_name: "1.3.6.1.4.1.14988.1.1.6.1.0",
              sensor_type: "count",
              sensor_descr: "DHCP Leases",
              sensor_unit: "",
              sensor_divisor: 1
            }
          ],
          count_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.14988.1.1.6.1",
              sensor_type: "count",
              sensor_descr: "DHCP Lease Count",
              sensor_unit: "",
              sensor_divisor: 1,
              descr_oid: nil
            }
          ]
        })

      stub(SnmpMock, :get, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.14988.1.1.6.1.0" -> {:ok, {:integer, 5}}
          _ -> {:error, :no_such_object}
        end
      end)

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.14988.1.1.6.1" ->
            # Walk returns OIDs with leading dot (gosnmp/agent format)
            {:ok,
             [
               %{oid: ".1.3.6.1.4.1.14988.1.1.6.1.0", value: {:integer, 5}}
             ]}

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

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

      # Find all sensors for this OID (with or without leading dot)
      dhcp_sensors =
        Enum.filter(sensors, fn s ->
          String.trim_leading(s.sensor_oid, ".") == "1.3.6.1.4.1.14988.1.1.6.1.0"
        end)

      assert length(dhcp_sensors) == 1,
             "Expected 1 sensor but got #{length(dhcp_sensors)}: #{inspect(Enum.map(dhcp_sensors, & &1.sensor_descr))}"
    end

    test "discovers processor_oids as table sensors" do
      profile =
        base_profile(%{
          processor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.5.1",
              sensor_type: "processor",
              sensor_descr: "CPU Load",
              sensor_unit: "%",
              sensor_divisor: 1,
              descr_oid: nil
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.5.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.4.1.99999.5.1.1", value: {:integer, 25}},
               %{oid: "1.3.6.1.4.1.99999.5.1.2", value: {:integer, 40}}
             ]}

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

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

      processors = Enum.filter(sensors, &(&1.sensor_type == "processor"))
      assert length(processors) == 2
      assert Enum.any?(processors, &(&1.last_value == 25))
      assert Enum.any?(processors, &(&1.last_value == 40))
    end
  end

  describe "discover_sensors/2 sensor description building" do
    test "uses sensor_type when description contains MIB symbolic name" do
      profile =
        base_profile(%{
          table_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.3.1",
              sensor_type: "temperature",
              sensor_descr: "MIKROTIK-MIB::mtxrOpticalName",
              sensor_unit: "C",
              sensor_divisor: 1,
              descr_oid: nil
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.3.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.4.1.99999.3.1.1", value: {:integer, 42}}
             ]}

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

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

      sensor = Enum.find(sensors, &(&1.sensor_type == "temperature"))
      # Description should fall back to sensor_type, not use MIB name
      assert sensor.sensor_descr == "Temperature"
      refute String.contains?(sensor.sensor_descr, "::")
    end

    test "uses oid_name for description when sensor_descr is nil" do
      profile =
        base_profile(%{
          table_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.3.1",
              sensor_type: "temperature",
              oid_name: "boardTemp",
              sensor_unit: "C",
              sensor_divisor: 1,
              descr_oid: nil
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.3.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.4.1.99999.3.1.1", value: {:integer, 42}}
             ]}

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

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

      sensor = Enum.find(sensors, &(&1.sensor_type == "temperature"))
      assert sensor.sensor_descr == "Boardtemp"
    end

    test "does not append index when only one sensor instance exists" do
      profile =
        base_profile(%{
          table_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.3.1",
              sensor_type: "temperature",
              sensor_descr: "CPU Temp",
              sensor_unit: "C",
              sensor_divisor: 1,
              descr_oid: nil
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.3.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.4.1.99999.3.1.1", value: {:integer, 42}}
             ]}

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

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

      sensor = Enum.find(sensors, &(&1.sensor_type == "temperature"))
      # Single instance should not have index appended
      assert sensor.sensor_descr == "Cpu temp"
    end

    test "appends index for multiple sensor instances" do
      profile =
        base_profile(%{
          table_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.3.1",
              sensor_type: "temperature",
              sensor_descr: "Temp",
              sensor_unit: "C",
              sensor_divisor: 1,
              descr_oid: nil
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.3.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.4.1.99999.3.1.1", value: {:integer, 42}},
               %{oid: "1.3.6.1.4.1.99999.3.1.2", value: {:integer, 55}},
               %{oid: "1.3.6.1.4.1.99999.3.1.3", value: {:integer, 38}}
             ]}

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

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

      temp_sensors =
        sensors
        |> Enum.filter(&(&1.sensor_type == "temperature"))
        |> Enum.sort_by(& &1.sensor_index)

      assert length(temp_sensors) == 3
      # First one has no index suffix
      assert Enum.at(temp_sensors, 0).sensor_descr == "Temp"
      # Second and third have index suffix
      assert Enum.at(temp_sensors, 1).sensor_descr == "Temp 2"
      assert Enum.at(temp_sensors, 2).sensor_descr == "Temp 3"
    end

    test "uses default sensor_divisor of 1 when not specified" do
      profile =
        base_profile(%{
          table_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.3.1",
              sensor_type: "temperature",
              sensor_descr: "Temp",
              descr_oid: nil
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.3.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.4.1.99999.3.1.1", value: {:integer, 42}}
             ]}

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

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

      sensor = hd(sensors)
      assert sensor.sensor_divisor == 1
      assert sensor.sensor_unit == ""
    end
  end

  describe "discover_wireless_sensors/2" do
    test "returns empty list for unknown profile" do
      profile = base_profile(%{name: "nonexistent_vendor_xyz"})

      assert {:ok, sensors} = Dynamic.discover_wireless_sensors(profile, @client_opts)
      assert sensors == []
    end

    test "returns empty list when profile has no name key" do
      profile = %{vendor: "TestVendor"}

      assert {:ok, sensors} = Dynamic.discover_wireless_sensors(profile, @client_opts)
      assert sensors == []
    end
  end

  describe "discover_interfaces/2" do
    test "delegates to Base.discover_interfaces" do
      # Mock ifIndex walk - empty means no interfaces
      expect(SnmpMock, :walk, fn _, "1.3.6.1.2.1.2.2.1.1", _ ->
        {:ok, []}
      end)

      profile = base_profile()

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

  describe "discover_sensors/2 with index templates" do
    test "uses index_template from YAML to create unique sensor_index values" do
      # This simulates MikroTik firewall connection count sensors which
      # are scalar values (.0) but need unique indices
      profile =
        base_profile(%{
          count_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.14988.1.1.22.1.1",
              sensor_type: "count",
              sensor_descr: "Total number of connections",
              sensor_unit: "",
              sensor_divisor: 1,
              descr_oid: nil,
              index_template: "mtxrCtTotalEntries.{{ $index }}"
            },
            %{
              base_oid: "1.3.6.1.4.1.14988.1.1.22.1.2",
              sensor_type: "count",
              sensor_descr: "Total number of ipv4 connections",
              sensor_unit: "",
              sensor_divisor: 1,
              descr_oid: nil,
              index_template: "mtxrCtIP4Entries.{{ $index }}"
            },
            %{
              base_oid: "1.3.6.1.4.1.14988.1.1.22.1.3",
              sensor_type: "count",
              sensor_descr: "Total number of ipv6 connections",
              sensor_unit: "",
              sensor_divisor: 1,
              descr_oid: nil,
              index_template: "mtxrCtIP6Entries.{{ $index }}"
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.14988.1.1.22.1.1" ->
            {:ok, [%{oid: "1.3.6.1.4.1.14988.1.1.22.1.1.0", value: {:integer, 1270}}]}

          "1.3.6.1.4.1.14988.1.1.22.1.2" ->
            {:ok, [%{oid: "1.3.6.1.4.1.14988.1.1.22.1.2.0", value: {:integer, 1270}}]}

          "1.3.6.1.4.1.14988.1.1.22.1.3" ->
            {:ok, [%{oid: "1.3.6.1.4.1.14988.1.1.22.1.3.0", value: {:integer, 0}}]}

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

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

      count_sensors = Enum.filter(sensors, &(&1.sensor_type == "count"))
      assert length(count_sensors) == 3

      # Each sensor should have a unique index based on the template
      indices = count_sensors |> Enum.map(& &1.sensor_index) |> Enum.sort()
      assert indices == ["mtxrCtIP4Entries.0", "mtxrCtIP6Entries.0", "mtxrCtTotalEntries.0"]

      # Verify the values are correct
      total = Enum.find(count_sensors, &(&1.sensor_index == "mtxrCtTotalEntries.0"))
      assert total.last_value == 1270
      assert total.sensor_descr == "Total number of connections"

      ipv4 = Enum.find(count_sensors, &(&1.sensor_index == "mtxrCtIP4Entries.0"))
      assert ipv4.last_value == 1270
      assert ipv4.sensor_descr == "Total number of ipv4 connections"

      ipv6 = Enum.find(count_sensors, &(&1.sensor_index == "mtxrCtIP6Entries.0"))
      assert ipv6.last_value == 0
      assert ipv6.sensor_descr == "Total number of ipv6 connections"
    end

    test "falls back to default index when index_template is nil" do
      # When walking a single OID with multiple results, enumeration creates unique indices
      profile =
        base_profile(%{
          count_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.1.1",
              sensor_type: "count",
              sensor_descr: "Counter",
              sensor_unit: "",
              sensor_divisor: 1,
              descr_oid: nil,
              index_template: nil
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.1.1" ->
            {:ok,
             [
               %{oid: "1.3.6.1.4.1.99999.1.1.1", value: {:integer, 42}},
               %{oid: "1.3.6.1.4.1.99999.1.1.2", value: {:integer, 99}}
             ]}

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

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

      count_sensors = Enum.filter(sensors, &(&1.sensor_type == "count"))
      assert length(count_sensors) == 2

      # Should use default "count_1", "count_2" indices from enumeration
      indices = count_sensors |> Enum.map(& &1.sensor_index) |> Enum.sort()
      assert indices == ["count_1", "count_2"]
    end

    test "handles numeric index_template from YAML without crashing" do
      # YAML parses bare numbers like `index: 0` as integers, not strings.
      # This must not crash String.replace/4.
      profile =
        base_profile(%{
          count_sensor_oids: [
            %{
              base_oid: "1.3.6.1.4.1.99999.1.1",
              sensor_type: "count",
              sensor_descr: "Memory Usage",
              sensor_unit: "",
              sensor_divisor: 1,
              descr_oid: nil,
              index_template: 0
            }
          ]
        })

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

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.99999.1.1" ->
            {:ok, [%{oid: "1.3.6.1.4.1.99999.1.1.1", value: {:integer, 42}}]}

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

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

      count_sensors = Enum.filter(sensors, &(&1.sensor_type == "count"))
      assert length(count_sensors) == 1
      assert hd(count_sensors).sensor_index == "0"
    end
  end
end
