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

  import Mox

  alias Towerops.Snmp.Profiles.Vendors.Vendor
  alias Towerops.Snmp.SnmpMock

  setup :verify_on_exit!

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

  @sample_sensor_def %{
    oid: "1.3.6.1.4.1.17713.21.1.2.1.0",
    sensor_type: "rssi",
    sensor_descr: "RSSI",
    sensor_unit: "dBm",
    sensor_divisor: 1
  }

  describe "fetch_sensors/2" do
    test "returns sensors for all defs that respond with integer values" do
      defs = [
        %{
          oid: "1.3.6.1.4.1.100.1.0",
          sensor_type: "rssi",
          sensor_descr: "RSSI",
          sensor_unit: "dBm",
          sensor_divisor: 1
        },
        %{
          oid: "1.3.6.1.4.1.100.2.0",
          sensor_type: "snr",
          sensor_descr: "SNR",
          sensor_unit: "dB",
          sensor_divisor: 1
        }
      ]

      expect(SnmpMock, :get, 2, fn _, _oid, _ -> {:ok, -65} end)

      sensors = Vendor.fetch_sensors(defs, @client_opts)

      assert length(sensors) == 2
      assert Enum.all?(sensors, &(&1.last_value == -65))
    end

    test "filters out sensors that fail to respond" do
      defs = [
        %{
          oid: "1.3.6.1.4.1.100.1.0",
          sensor_type: "rssi",
          sensor_descr: "RSSI",
          sensor_unit: "dBm",
          sensor_divisor: 1
        },
        %{
          oid: "1.3.6.1.4.1.100.2.0",
          sensor_type: "snr",
          sensor_descr: "SNR",
          sensor_unit: "dB",
          sensor_divisor: 1
        }
      ]

      expect(SnmpMock, :get, fn _, "1.3.6.1.4.1.100.1.0", _ -> {:ok, -65} end)
      expect(SnmpMock, :get, fn _, "1.3.6.1.4.1.100.2.0", _ -> {:error, :timeout} end)

      sensors = Vendor.fetch_sensors(defs, @client_opts)

      assert length(sensors) == 1
      assert hd(sensors).sensor_type == "rssi"
    end

    test "returns empty list when all sensors fail" do
      defs = [
        %{
          oid: "1.3.6.1.4.1.100.1.0",
          sensor_type: "rssi",
          sensor_descr: "RSSI",
          sensor_unit: "dBm",
          sensor_divisor: 1
        }
      ]

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

      assert Vendor.fetch_sensors(defs, @client_opts) == []
    end

    test "returns empty list for empty defs" do
      assert Vendor.fetch_sensors([], @client_opts) == []
    end
  end

  describe "fetch_sensor_value/2" do
    test "returns sensor map with correct fields when SNMP returns integer" do
      expect(SnmpMock, :get, fn _, "1.3.6.1.4.1.17713.21.1.2.1.0", _ -> {:ok, -65} end)

      sensor = Vendor.fetch_sensor_value(@sample_sensor_def, @client_opts)

      assert sensor.sensor_type == "rssi"
      assert sensor.sensor_oid == "1.3.6.1.4.1.17713.21.1.2.1.0"
      assert sensor.sensor_descr == "RSSI"
      assert sensor.sensor_unit == "dBm"
      assert sensor.sensor_divisor == 1
      assert sensor.last_value == -65
    end

    test "generates sensor_index from description slug and OID suffix" do
      expect(SnmpMock, :get, fn _, _, _ -> {:ok, 42} end)

      sensor = Vendor.fetch_sensor_value(@sample_sensor_def, @client_opts)

      # OID "1.3.6.1.4.1.17713.21.1.2.1.0" -> last 2 parts "1_0"
      # Description "RSSI" -> slug "rssi"
      assert sensor.sensor_index == "wireless_rssi_1_0"
    end

    test "slugifies multi-word descriptions" do
      sensor_def = %{
        oid: "1.3.6.1.4.1.100.5.3.0",
        sensor_type: "tx_power",
        sensor_descr: "Tx Power Level",
        sensor_unit: "dBm",
        sensor_divisor: 1
      }

      expect(SnmpMock, :get, fn _, _, _ -> {:ok, 20} end)

      sensor = Vendor.fetch_sensor_value(sensor_def, @client_opts)

      assert sensor.sensor_index == "wireless_tx_power_level_3_0"
    end

    test "returns nil when SNMP returns an error" do
      expect(SnmpMock, :get, fn _, _, _ -> {:error, :timeout} end)

      assert Vendor.fetch_sensor_value(@sample_sensor_def, @client_opts) == nil
    end

    test "returns nil when SNMP returns no_such_object" do
      expect(SnmpMock, :get, fn _, _, _ -> {:error, :no_such_object} end)

      assert Vendor.fetch_sensor_value(@sample_sensor_def, @client_opts) == nil
    end

    test "returns nil when SNMP returns a string value instead of integer" do
      expect(SnmpMock, :get, fn _, _, _ -> {:ok, "not_an_integer"} end)

      assert Vendor.fetch_sensor_value(@sample_sensor_def, @client_opts) == nil
    end

    test "returns nil when SNMP returns nil value" do
      expect(SnmpMock, :get, fn _, _, _ -> {:ok, nil} end)

      assert Vendor.fetch_sensor_value(@sample_sensor_def, @client_opts) == nil
    end

    test "handles zero integer value" do
      expect(SnmpMock, :get, fn _, _, _ -> {:ok, 0} end)

      sensor = Vendor.fetch_sensor_value(@sample_sensor_def, @client_opts)

      assert sensor.last_value == 0
    end

    test "handles negative integer value" do
      expect(SnmpMock, :get, fn _, _, _ -> {:ok, -90} end)

      sensor = Vendor.fetch_sensor_value(@sample_sensor_def, @client_opts)

      assert sensor.last_value == -90
    end
  end

  describe "walk_table_sensors/3" do
    test "returns sensors from walk results with integer values" do
      base_oid = "1.3.6.1.4.1.100.1"

      sensor_def = %{
        sensor_type: "client_rssi",
        sensor_descr: "Client RSSI",
        sensor_unit: "dBm",
        sensor_divisor: 1
      }

      expect(SnmpMock, :walk, fn _, "1.3.6.1.4.1.100.1", _ ->
        {:ok,
         [
           %{oid: "1.3.6.1.4.1.100.1.1", value: -55},
           %{oid: "1.3.6.1.4.1.100.1.2", value: -70}
         ]}
      end)

      sensors = Vendor.walk_table_sensors(base_oid, sensor_def, @client_opts)

      assert length(sensors) == 2

      [first, second] = sensors

      assert first.sensor_type == "client_rssi"
      assert first.sensor_index == "client_rssi_1"
      assert first.sensor_oid == "1.3.6.1.4.1.100.1.1"
      assert first.sensor_descr == "Client RSSI 1"
      assert first.sensor_unit == "dBm"
      assert first.sensor_divisor == 1
      assert first.last_value == -55

      assert second.sensor_index == "client_rssi_2"
      assert second.sensor_oid == "1.3.6.1.4.1.100.1.2"
      assert second.sensor_descr == "Client RSSI 2"
      assert second.last_value == -70
    end

    test "returns empty list when walk returns error" do
      sensor_def = %{
        sensor_type: "client_rssi",
        sensor_descr: "Client RSSI",
        sensor_unit: "dBm",
        sensor_divisor: 1
      }

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

      assert Vendor.walk_table_sensors("1.3.6.1.4.1.100.1", sensor_def, @client_opts) == []
    end

    test "returns empty list when walk returns empty map" do
      sensor_def = %{
        sensor_type: "client_rssi",
        sensor_descr: "Client RSSI",
        sensor_unit: "dBm",
        sensor_divisor: 1
      }

      expect(SnmpMock, :walk, fn _, _, _ -> {:ok, [%{oid: "x", value: "y"}]} end)

      # Client.walk converts list to map, so the Vendor module receives a map
      # But we need to mock at the SnmpMock level. Client.walk returns {:ok, map}
      # The walk results go through Client.walk which converts to a map.
      # Let's mock so it returns an empty map after conversion.
      assert Vendor.walk_table_sensors("1.3.6.1.4.1.100.1", sensor_def, @client_opts) == []
    end

    test "filters out non-integer values from walk results" do
      sensor_def = %{
        sensor_type: "client_rssi",
        sensor_descr: "Client RSSI",
        sensor_unit: "dBm",
        sensor_divisor: 1
      }

      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok,
         [
           %{oid: "1.3.6.1.4.1.100.1.1", value: -55},
           %{oid: "1.3.6.1.4.1.100.1.2", value: "not_integer"},
           %{oid: "1.3.6.1.4.1.100.1.3", value: -70}
         ]}
      end)

      sensors = Vendor.walk_table_sensors("1.3.6.1.4.1.100.1", sensor_def, @client_opts)

      # Non-integer entries are filtered out by build_table_sensor returning nil
      assert length(sensors) == 2
      assert Enum.at(sensors, 0).last_value == -55
      assert Enum.at(sensors, 1).last_value == -70
    end

    test "indexes start at 1 and increment sequentially" do
      sensor_def = %{
        sensor_type: "tx_power",
        sensor_descr: "Tx Power",
        sensor_unit: "dBm",
        sensor_divisor: 1
      }

      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok,
         [
           %{oid: "1.3.6.1.4.1.100.1.1", value: 20},
           %{oid: "1.3.6.1.4.1.100.1.2", value: 21},
           %{oid: "1.3.6.1.4.1.100.1.3", value: 22}
         ]}
      end)

      sensors = Vendor.walk_table_sensors("1.3.6.1.4.1.100.1", sensor_def, @client_opts)

      assert Enum.at(sensors, 0).sensor_index == "tx_power_1"
      assert Enum.at(sensors, 0).sensor_descr == "Tx Power 1"
      assert Enum.at(sensors, 1).sensor_index == "tx_power_2"
      assert Enum.at(sensors, 1).sensor_descr == "Tx Power 2"
      assert Enum.at(sensors, 2).sensor_index == "tx_power_3"
      assert Enum.at(sensors, 2).sensor_descr == "Tx Power 3"
    end

    test "returns empty list when walk returns no_such_object" do
      sensor_def = %{
        sensor_type: "client_rssi",
        sensor_descr: "Client RSSI",
        sensor_unit: "dBm",
        sensor_divisor: 1
      }

      expect(SnmpMock, :walk, fn _, _, _ -> {:error, :no_such_object} end)

      assert Vendor.walk_table_sensors("1.3.6.1.4.1.100.1", sensor_def, @client_opts) == []
    end

    test "handles single entry walk result" do
      sensor_def = %{
        sensor_type: "channel",
        sensor_descr: "Channel",
        sensor_unit: "MHz",
        sensor_divisor: 1
      }

      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, [%{oid: "1.3.6.1.4.1.100.1.1", value: 5180}]}
      end)

      sensors = Vendor.walk_table_sensors("1.3.6.1.4.1.100.1", sensor_def, @client_opts)

      assert length(sensors) == 1
      assert hd(sensors).sensor_index == "channel_1"
      assert hd(sensors).last_value == 5180
    end

    test "handles zero value in walk results" do
      sensor_def = %{
        sensor_type: "noise",
        sensor_descr: "Noise Floor",
        sensor_unit: "dBm",
        sensor_divisor: 1
      }

      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, [%{oid: "1.3.6.1.4.1.100.1.1", value: 0}]}
      end)

      sensors = Vendor.walk_table_sensors("1.3.6.1.4.1.100.1", sensor_def, @client_opts)

      assert length(sensors) == 1
      assert hd(sensors).last_value == 0
    end
  end
end
