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

  import Mox

  alias Towerops.Snmp.Profiles.Vendors.Routeros
  alias Towerops.Snmp.SnmpMock

  setup :verify_on_exit!

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

  # OID constants from the module
  @license_level_oid "1.3.6.1.4.1.14988.1.1.4.3.0"
  @version_oid "1.3.6.1.4.1.14988.1.1.4.4.0"
  @serial_number_oid "1.3.6.1.4.1.14988.1.1.7.3.0"
  @wl_ap_table "1.3.6.1.4.1.14988.1.1.1.3.1"
  @wl_stat_table "1.3.6.1.4.1.14988.1.1.1.1.1"
  @wl_60g_table "1.3.6.1.4.1.14988.1.1.1.8.1"
  @lte_modem_table "1.3.6.1.4.1.14988.1.1.16.1.1"
  @gauge_table "1.3.6.1.4.1.14988.1.1.3.100.1"
  @optical_table "1.3.6.1.4.1.14988.1.1.19.1.1"

  describe "profile_names/0" do
    test "returns expected profile names" do
      assert Routeros.profile_names() == ["routeros"]
    end
  end

  describe "detect_hardware/1" do
    test "returns RouterOS Level when SNMP responds with integer license" do
      expect(SnmpMock, :get, fn _, @license_level_oid, _ ->
        {:ok, 6}
      end)

      assert Routeros.detect_hardware(@client_opts) == "RouterOS Level 6"
    end

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

      assert Routeros.detect_hardware(@client_opts) == nil
    end

    test "returns nil when license is not an integer" do
      expect(SnmpMock, :get, fn _, @license_level_oid, _ ->
        {:ok, "not an integer"}
      end)

      assert Routeros.detect_hardware(@client_opts) == nil
    end
  end

  describe "detect_version/1" do
    test "returns RouterOS version with prefix and license level when both available" do
      expect(SnmpMock, :get, 2, fn
        _, @version_oid, _ -> {:ok, "7.14.1"}
        _, @license_level_oid, _ -> {:ok, 6}
      end)

      assert Routeros.detect_version(@client_opts) == "RouterOS 7.14.1 (Level 6)"
    end

    test "returns RouterOS version without license level when level unavailable" do
      expect(SnmpMock, :get, 2, fn
        _, @version_oid, _ -> {:ok, "7.14.1"}
        _, @license_level_oid, _ -> {:error, :timeout}
      end)

      assert Routeros.detect_version(@client_opts) == "RouterOS 7.14.1"
    end

    test "handles different version formats" do
      expect(SnmpMock, :get, 2, fn
        _, @version_oid, _ -> {:ok, "6.48.3"}
        _, @license_level_oid, _ -> {:ok, 4}
      end)

      assert Routeros.detect_version(@client_opts) == "RouterOS 6.48.3 (Level 4)"
    end

    test "returns nil when version is empty string" do
      expect(SnmpMock, :get, fn _, @version_oid, _ ->
        {:ok, ""}
      end)

      assert Routeros.detect_version(@client_opts) == nil
    end

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

      assert Routeros.detect_version(@client_opts) == nil
    end

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

      assert Routeros.detect_version(@client_opts) == nil
    end

    test "returns nil when version is not a string" do
      expect(SnmpMock, :get, fn _, @version_oid, _ ->
        {:ok, 123}
      end)

      assert Routeros.detect_version(@client_opts) == nil
    end

    test "handles version returned as list of integers" do
      expect(SnmpMock, :get, 2, fn
        _, @version_oid, _ -> {:ok, [7, 20, 6]}
        _, @license_level_oid, _ -> {:ok, 6}
      end)

      assert Routeros.detect_version(@client_opts) == "RouterOS 7.20.6 (Level 6)"
    end

    test "handles version returned as list of integers without license level" do
      expect(SnmpMock, :get, 2, fn
        _, @version_oid, _ -> {:ok, [6, 48, 3]}
        _, @license_level_oid, _ -> {:error, :timeout}
      end)

      assert Routeros.detect_version(@client_opts) == "RouterOS 6.48.3"
    end
  end

  describe "detect_serial_number/1" do
    test "returns serial number when SNMP responds" do
      expect(SnmpMock, :get, fn _, @serial_number_oid, _ ->
        {:ok, "HC907K5P55K"}
      end)

      assert Routeros.detect_serial_number(@client_opts) == "HC907K5P55K"
    end

    test "returns nil when serial number is empty string" do
      expect(SnmpMock, :get, fn _, @serial_number_oid, _ ->
        {:ok, ""}
      end)

      assert Routeros.detect_serial_number(@client_opts) == nil
    end

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

      assert Routeros.detect_serial_number(@client_opts) == nil
    end

    test "returns nil when serial number is not a string" do
      expect(SnmpMock, :get, fn _, @serial_number_oid, _ ->
        {:ok, 12_345}
      end)

      assert Routeros.detect_serial_number(@client_opts) == nil
    end
  end

  describe "wireless_oid_defs/0" do
    test "returns list of sensor definitions" do
      defs = Routeros.wireless_oid_defs()

      assert is_list(defs)
      assert length(defs) == 1
    end

    test "each definition has required fields" do
      defs = Routeros.wireless_oid_defs()

      Enum.each(defs, fn def ->
        assert Map.has_key?(def, :oid)
        assert Map.has_key?(def, :sensor_descr)
        assert Map.has_key?(def, :sensor_type)
        assert Map.has_key?(def, :sensor_unit)
        assert Map.has_key?(def, :sensor_divisor)
      end)
    end

    test "includes PPPoE Sessions sensor" do
      defs = Routeros.wireless_oid_defs()
      pppoe = Enum.find(defs, &(&1.sensor_descr == "PPPoE Sessions"))

      assert pppoe
      assert pppoe.sensor_type == "count"
      assert pppoe.oid == "1.3.6.1.4.1.9.9.150.1.1.1.0"
    end
  end

  describe "discover_wireless_sensors/1" do
    test "discovers sensors from AP table when SNMP responds" do
      # Mock walk calls - snmpkit returns list of maps, Client converts to map
      expect(SnmpMock, :walk, fn _, @wl_ap_table, _ ->
        {:ok,
         [
           %{oid: "#{@wl_ap_table}.5.1", value: "TestSSID"},
           %{oid: "#{@wl_ap_table}.6.1", value: 5},
           %{oid: "#{@wl_ap_table}.7.1", value: 5180},
           %{oid: "#{@wl_ap_table}.9.1", value: -95},
           %{oid: "#{@wl_ap_table}.10.1", value: 85},
           %{oid: "#{@wl_ap_table}.2.1", value: 300_000_000},
           %{oid: "#{@wl_ap_table}.3.1", value: 150_000_000}
         ]}
      end)

      expect(SnmpMock, :walk, fn _, @wl_stat_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @wl_60g_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @lte_modem_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @gauge_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:ok, []} end)

      # System sensors (PPPoE)
      expect(SnmpMock, :get, fn _, _oid, _ -> {:ok, 100} end)

      sensors = Routeros.discover_wireless_sensors(@client_opts)

      assert is_list(sensors)
      # 6 AP sensors + 1 system sensor
      assert length(sensors) == 7

      # Verify AP sensor types are present
      sensor_types = Enum.map(sensors, & &1.sensor_type)
      assert "clients" in sensor_types
      assert "frequency" in sensor_types
      assert "noise-floor" in sensor_types
      assert "ccq" in sensor_types
      assert "rate" in sensor_types
      assert "count" in sensor_types
    end

    test "discovers sensors from station table" do
      expect(SnmpMock, :walk, fn _, @wl_ap_table, _ -> {:ok, []} end)

      expect(SnmpMock, :walk, fn _, @wl_stat_table, _ ->
        {:ok,
         [
           %{oid: "#{@wl_stat_table}.5.1", value: "ClientSSID"},
           %{oid: "#{@wl_stat_table}.7.1", value: 2437},
           %{oid: "#{@wl_stat_table}.9.1", value: 95},
           %{oid: "#{@wl_stat_table}.10.1", value: 90},
           %{oid: "#{@wl_stat_table}.2.1", value: 144_400_000},
           %{oid: "#{@wl_stat_table}.3.1", value: 72_200_000}
         ]}
      end)

      expect(SnmpMock, :walk, fn _, @wl_60g_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @lte_modem_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @gauge_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:ok, []} end)
      expect(SnmpMock, :get, fn _, _oid, _ -> {:ok, 50} end)

      sensors = Routeros.discover_wireless_sensors(@client_opts)

      assert is_list(sensors)
      # 5 station sensors + 1 system sensor
      assert length(sensors) == 6

      sensor_types = Enum.map(sensors, & &1.sensor_type)
      assert "frequency" in sensor_types
      assert "ccq" in sensor_types
      assert "rate" in sensor_types
    end

    test "discovers sensors from 60G table" do
      expect(SnmpMock, :walk, fn _, @wl_ap_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @wl_stat_table, _ -> {:ok, []} end)

      expect(SnmpMock, :walk, fn _, @wl_60g_table, _ ->
        {:ok,
         [
           %{oid: "#{@wl_60g_table}.2.1", value: "60G-Link"},
           %{oid: "#{@wl_60g_table}.6.1", value: 60_480},
           %{oid: "#{@wl_60g_table}.8.1", value: 75},
           %{oid: "#{@wl_60g_table}.12.1", value: -55},
           %{oid: "#{@wl_60g_table}.13.1", value: 1800}
         ]}
      end)

      expect(SnmpMock, :walk, fn _, @lte_modem_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @gauge_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:ok, []} end)
      expect(SnmpMock, :get, fn _, _oid, _ -> {:ok, 25} end)

      sensors = Routeros.discover_wireless_sensors(@client_opts)

      assert is_list(sensors)
      # 4 60G sensors + 1 system sensor
      assert length(sensors) == 5

      sensor_types = Enum.map(sensors, & &1.sensor_type)
      assert "frequency" in sensor_types
      assert "quality" in sensor_types
      assert "rssi" in sensor_types
      assert "rate" in sensor_types
    end

    test "discovers sensors from LTE table" do
      expect(SnmpMock, :walk, fn _, @wl_ap_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @wl_stat_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @wl_60g_table, _ -> {:ok, []} end)

      expect(SnmpMock, :walk, fn _, @lte_modem_table, _ ->
        {:ok,
         [
           %{oid: "#{@lte_modem_table}.3.1", value: -12},
           %{oid: "#{@lte_modem_table}.4.1", value: -80},
           %{oid: "#{@lte_modem_table}.7.1", value: 15}
         ]}
      end)

      expect(SnmpMock, :walk, fn _, @gauge_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:ok, []} end)
      expect(SnmpMock, :get, fn _, _oid, _ -> {:ok, 10} end)

      sensors = Routeros.discover_wireless_sensors(@client_opts)

      assert is_list(sensors)
      # 3 LTE sensors + 1 system sensor
      assert length(sensors) == 4

      sensor_types = Enum.map(sensors, & &1.sensor_type)
      assert "rsrq" in sensor_types
      assert "rsrp" in sensor_types
      assert "sinr" in sensor_types
    end

    test "returns only system sensors when all tables are empty" do
      expect(SnmpMock, :walk, fn _, @wl_ap_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @wl_stat_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @wl_60g_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @lte_modem_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @gauge_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:ok, []} end)

      # System sensors (PPPoE only - DHCP leases handled by YAML profile)
      expect(SnmpMock, :get, fn _, _oid, _ -> {:ok, 100} end)

      sensors = Routeros.discover_wireless_sensors(@client_opts)

      assert is_list(sensors)
      assert length(sensors) == 1

      descriptions = Enum.map(sensors, & &1.sensor_descr)
      assert "PPPoE Sessions" in descriptions
    end

    test "returns empty list when all SNMP calls fail" do
      expect(SnmpMock, :walk, fn _, @wl_ap_table, _ -> {:error, :timeout} end)
      expect(SnmpMock, :walk, fn _, @wl_stat_table, _ -> {:error, :timeout} end)
      expect(SnmpMock, :walk, fn _, @wl_60g_table, _ -> {:error, :timeout} end)
      expect(SnmpMock, :walk, fn _, @lte_modem_table, _ -> {:error, :timeout} end)
      expect(SnmpMock, :walk, fn _, @gauge_table, _ -> {:error, :timeout} end)
      expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:error, :timeout} end)
      expect(SnmpMock, :get, fn _, _, _ -> {:error, :timeout} end)

      sensors = Routeros.discover_wireless_sensors(@client_opts)

      assert sensors == []
    end

    test "discovers gauge sensors with correct divisors for voltage" do
      expect(SnmpMock, :walk, fn _, @wl_ap_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @wl_stat_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @wl_60g_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @lte_modem_table, _ -> {:ok, []} end)

      # mtxrGaugeTable with voltage sensor (unit type 3 = deci-volts)
      # .2.{idx} = mtxrGaugeName, .3.{idx} = mtxrGaugeValue, .4.{idx} = mtxrGaugeUnit
      expect(SnmpMock, :walk, fn _, @gauge_table, _ ->
        {:ok,
         [
           # PSU Voltage: raw value 469, unit type 3 (deci-volts) -> should be 46.9V
           %{oid: "#{@gauge_table}.2.1", value: "psu-voltage"},
           %{oid: "#{@gauge_table}.3.1", value: 469},
           %{oid: "#{@gauge_table}.4.1", value: 3},
           # CPU Temperature: raw value 450, unit type 1 (celsius) -> should be 45.0°C
           # Mikrotik reports temperature in tenths of degrees (divisor 10)
           %{oid: "#{@gauge_table}.2.2", value: "cpu-temperature"},
           %{oid: "#{@gauge_table}.3.2", value: 450},
           %{oid: "#{@gauge_table}.4.2", value: 1}
         ]}
      end)

      expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:ok, []} end)
      expect(SnmpMock, :get, fn _, _oid, _ -> {:error, :timeout} end)

      sensors = Routeros.discover_wireless_sensors(@client_opts)

      # Find the voltage sensor
      # Note: format_gauge_description prefixes with "System " for non-generic names
      voltage_sensor = Enum.find(sensors, &(&1.sensor_descr == "System psu-voltage"))
      assert voltage_sensor
      assert voltage_sensor.sensor_type == "voltage"
      assert voltage_sensor.sensor_unit == "V"
      assert voltage_sensor.sensor_divisor == 10
      assert voltage_sensor.last_value == 46.9

      # Find the temperature sensor
      # Mikrotik reports temperature in tenths of degrees (e.g., 450 = 45.0°C)
      temp_sensor = Enum.find(sensors, &(&1.sensor_descr == "System cpu-temperature"))
      assert temp_sensor
      assert temp_sensor.sensor_type == "temperature"
      assert temp_sensor.sensor_unit == "°C"
      assert temp_sensor.sensor_divisor == 10
      assert temp_sensor.last_value == 45.0
    end

    test "overrides sensor type based on name when unit type is unknown" do
      expect(SnmpMock, :walk, fn _, @wl_ap_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @wl_stat_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @wl_60g_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @lte_modem_table, _ -> {:ok, []} end)

      # Device reports wrong unit type (0 = unknown) but name contains "voltage"
      expect(SnmpMock, :walk, fn _, @gauge_table, _ ->
        {:ok,
         [
           %{oid: "#{@gauge_table}.2.1", value: "Board Voltage"},
           %{oid: "#{@gauge_table}.3.1", value: 120},
           %{oid: "#{@gauge_table}.4.1", value: 0}
         ]}
      end)

      expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:ok, []} end)
      expect(SnmpMock, :get, fn _, _oid, _ -> {:error, :timeout} end)

      sensors = Routeros.discover_wireless_sensors(@client_opts)

      voltage_sensor = Enum.find(sensors, &(&1.sensor_descr == "Board Voltage"))
      assert voltage_sensor
      # Should override to voltage based on name, with divisor 10
      assert voltage_sensor.sensor_type == "voltage"
      assert voltage_sensor.sensor_unit == "V"
      assert voltage_sensor.sensor_divisor == 10
      assert voltage_sensor.last_value == 12.0
    end

    test "overrides sensor type when device reports wrong unit type" do
      expect(SnmpMock, :walk, fn _, @wl_ap_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @wl_stat_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @wl_60g_table, _ -> {:ok, []} end)
      expect(SnmpMock, :walk, fn _, @lte_modem_table, _ -> {:ok, []} end)

      # Device incorrectly reports unit type 1 (celsius/temperature) for voltage sensor
      # This is the real-world bug case: sensor named "psu2-voltage" with unit type 1
      expect(SnmpMock, :walk, fn _, @gauge_table, _ ->
        {:ok,
         [
           # PSU voltage sensor incorrectly reported as temperature (unit type 1)
           %{oid: "#{@gauge_table}.2.7201", value: "psu1-voltage"},
           %{oid: "#{@gauge_table}.3.7201", value: 469},
           %{oid: "#{@gauge_table}.4.7201", value: 1},
           # Another voltage sensor with wrong unit type
           %{oid: "#{@gauge_table}.2.7202", value: "psu2-voltage"},
           %{oid: "#{@gauge_table}.3.7202", value: 469},
           %{oid: "#{@gauge_table}.4.7202", value: 1}
         ]}
      end)

      expect(SnmpMock, :walk, fn _, @optical_table, _ -> {:ok, []} end)
      expect(SnmpMock, :get, fn _, _oid, _ -> {:error, :timeout} end)

      sensors = Routeros.discover_wireless_sensors(@client_opts)

      # Verify both sensors are correctly overridden to voltage with divisor 10
      psu1_sensor = Enum.find(sensors, &(&1.sensor_descr == "System psu1-voltage"))
      assert psu1_sensor
      assert psu1_sensor.sensor_type == "voltage"
      assert psu1_sensor.sensor_unit == "V"
      assert psu1_sensor.sensor_divisor == 10
      assert psu1_sensor.last_value == 46.9

      psu2_sensor = Enum.find(sensors, &(&1.sensor_descr == "System psu2-voltage"))
      assert psu2_sensor
      assert psu2_sensor.sensor_type == "voltage"
      assert psu2_sensor.sensor_unit == "V"
      assert psu2_sensor.sensor_divisor == 10
      assert psu2_sensor.last_value == 46.9
    end
  end
end
