defmodule Towerops.Snmp.Profiles.MikrotikTest do
  use ExUnit.Case, async: true
  use ExUnitProperties

  import Mox

  alias Towerops.Snmp.Profiles.Mikrotik
  alias Towerops.Snmp.SnmpMock

  setup :verify_on_exit!

  @test_opts [ip: "192.168.1.1", community: "public", version: "2c"]

  describe "discover_system_info/1" do
    test "merges base system info with MikroTik-specific info" do
      # Mock Base.discover_system_info (via Client.get_multiple for system OIDs)
      base_system_oids = [
        "1.3.6.1.2.1.1.1.0",
        "1.3.6.1.2.1.1.2.0",
        "1.3.6.1.2.1.1.3.0",
        "1.3.6.1.2.1.1.4.0",
        "1.3.6.1.2.1.1.5.0",
        "1.3.6.1.2.1.1.6.0"
      ]

      expect(SnmpMock, :get, length(base_system_oids), fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.1.1.0" ->
            {:ok, {:octet_string, "RouterOS RB5009UG+S+ (stable) 7.13.2"}}

          "1.3.6.1.2.1.1.2.0" ->
            {:ok, {:object_identifier, [1, 3, 6, 1, 4, 1, 14_988, 1]}}

          "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, "rb5009-main"}}

          "1.3.6.1.2.1.1.6.0" ->
            {:ok, {:octet_string, "Data Center"}}
        end
      end)

      # Mock MikroTik-specific OIDs
      mikrotik_oids = [
        "1.3.6.1.4.1.14988.1.1.7.3.0",
        "1.3.6.1.4.1.14988.1.1.7.4.0",
        "1.3.6.1.4.1.14988.1.1.7.9.0",
        "1.3.6.1.4.1.14988.1.1.7.8.0",
        "1.3.6.1.4.1.14988.1.1.4.4.0"
      ]

      expect(SnmpMock, :get, length(mikrotik_oids), fn _, oid, _ ->
        case oid do
          "1.3.6.1.4.1.14988.1.1.7.3.0" -> {:ok, {:octet_string, "ABC123DEF456"}}
          "1.3.6.1.4.1.14988.1.1.7.4.0" -> {:ok, {:octet_string, "7.13.2"}}
          "1.3.6.1.4.1.14988.1.1.7.9.0" -> {:ok, {:octet_string, "RB5009UG+S+"}}
          "1.3.6.1.4.1.14988.1.1.7.8.0" -> {:ok, {:octet_string, "RB5009"}}
          "1.3.6.1.4.1.14988.1.1.4.4.0" -> {:ok, {:octet_string, "level6"}}
        end
      end)

      assert {:ok, info} = Mikrotik.discover_system_info(@test_opts)

      # Check base info
      assert info.sys_descr == "RouterOS RB5009UG+S+ (stable) 7.13.2"
      assert info.sys_name == "rb5009-main"
      assert info.sys_location == "Data Center"

      # Check MikroTik-specific info
      assert info.serial_number == "ABC123DEF456"
      assert info.firmware_version == "7.13.2"
      assert info.board_name == "RB5009UG+S+"
    end

    test "handles missing MikroTik-specific OIDs gracefully" do
      # Mock Base system info
      expect(SnmpMock, :get, 6, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.1.1.0" -> {:ok, {:octet_string, "RouterOS Device"}}
          "1.3.6.1.2.1.1.2.0" -> {:ok, {:object_identifier, [1, 3, 6, 1, 4, 1, 14_988]}}
          "1.3.6.1.2.1.1.3.0" -> {:ok, {:timeticks, 12_345}}
          "1.3.6.1.2.1.1.4.0" -> {:ok, {:octet_string, "admin"}}
          "1.3.6.1.2.1.1.5.0" -> {:ok, {:octet_string, "router"}}
          "1.3.6.1.2.1.1.6.0" -> {:ok, {:octet_string, "office"}}
        end
      end)

      # Mock MikroTik OIDs returning error
      expect(SnmpMock, :get, 5, fn _, _, _ -> {:error, :no_such_object} end)
      assert {:ok, info} = Mikrotik.discover_system_info(@test_opts)

      # Should still have base info
      assert info.sys_descr == "RouterOS Device"
      assert info.sys_name == "router"
    end
  end

  describe "discover_interfaces/1" do
    test "delegates to Base.discover_interfaces" do
      # Mock IF-MIB walk for interface indices
      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: 1},
           %{oid: "1.3.6.1.2.1.2.2.1.1.2", value: 2}
         ]}
      end)

      # Mock interface data for each interface
      expect(SnmpMock, :get, 16, fn _, oid, _ ->
        cond do
          String.ends_with?(oid, ".2.2.1.2.1") -> {:ok, {:octet_string, "ether1"}}
          String.ends_with?(oid, ".2.2.1.2.2") -> {:ok, {:octet_string, "ether2"}}
          String.ends_with?(oid, ".2.2.1.3.1") -> {:ok, {:integer, 6}}
          String.ends_with?(oid, ".2.2.1.3.2") -> {:ok, {:integer, 6}}
          String.ends_with?(oid, ".2.2.1.5.1") -> {:ok, {:gauge32, 1_000_000_000}}
          String.ends_with?(oid, ".2.2.1.5.2") -> {:ok, {:gauge32, 1_000_000_000}}
          String.ends_with?(oid, ".2.2.1.6.1") -> {:ok, {:octet_string, <<0x00, 0x0C, 0x42, 0xAB, 0xCD, 0xEF>>}}
          String.ends_with?(oid, ".2.2.1.6.2") -> {:ok, {:octet_string, <<0x00, 0x0C, 0x42, 0x12, 0x34, 0x56>>}}
          String.ends_with?(oid, ".2.2.1.7.1") -> {:ok, {:integer, 1}}
          String.ends_with?(oid, ".2.2.1.7.2") -> {:ok, {:integer, 1}}
          String.ends_with?(oid, ".2.2.1.8.1") -> {:ok, {:integer, 1}}
          String.ends_with?(oid, ".2.2.1.8.2") -> {:ok, {:integer, 1}}
          String.ends_with?(oid, ".31.1.1.1.1.1") -> {:ok, {:octet_string, "ether1"}}
          String.ends_with?(oid, ".31.1.1.1.1.2") -> {:ok, {:octet_string, "ether2"}}
          String.ends_with?(oid, ".31.1.1.1.18.1") -> {:ok, {:octet_string, "WAN"}}
          String.ends_with?(oid, ".31.1.1.1.18.2") -> {:ok, {:octet_string, "LAN"}}
        end
      end)

      assert {:ok, interfaces} = Mikrotik.discover_interfaces(@test_opts)

      assert length(interfaces) == 2
      assert Enum.any?(interfaces, &(&1.if_descr == "ether1"))
      assert Enum.any?(interfaces, &(&1.if_descr == "ether2"))
    end
  end

  describe "discover_sensors/1" do
    test "discovers health sensors (temperature, voltage, fan)" do
      # Mock health sensor OIDs
      expect(SnmpMock, :get, 15, fn _, oid, _ ->
        case oid do
          # Voltages
          "1.3.6.1.4.1.14988.1.1.3.1.0" -> {:ok, {:integer, 12}}
          "1.3.6.1.4.1.14988.1.1.3.2.0" -> {:ok, {:integer, 33}}
          "1.3.6.1.4.1.14988.1.1.3.3.0" -> {:ok, {:integer, 50}}
          "1.3.6.1.4.1.14988.1.1.3.4.0" -> {:ok, {:integer, 120}}
          "1.3.6.1.4.1.14988.1.1.3.8.0" -> {:ok, {:integer, 240}}
          # Temperatures
          "1.3.6.1.4.1.14988.1.1.3.5.0" -> {:ok, {:integer, 450}}
          "1.3.6.1.4.1.14988.1.1.3.6.0" -> {:ok, {:integer, 550}}
          "1.3.6.1.4.1.14988.1.1.3.7.0" -> {:ok, {:integer, 480}}
          "1.3.6.1.4.1.14988.1.1.3.10.0" -> {:ok, {:integer, 500}}
          "1.3.6.1.4.1.14988.1.1.3.11.0" -> {:ok, {:integer, 520}}
          # Power & Current
          "1.3.6.1.4.1.14988.1.1.3.13.0" -> {:ok, {:integer, 1500}}
          "1.3.6.1.4.1.14988.1.1.3.12.0" -> {:ok, {:integer, 250}}
          # Fans
          "1.3.6.1.4.1.14988.1.1.3.17.0" -> {:ok, {:integer, 3000}}
          "1.3.6.1.4.1.14988.1.1.3.18.0" -> {:ok, {:integer, 0}}
          # Processor frequency
          "1.3.6.1.4.1.14988.1.1.3.14.0" -> {:ok, {:integer, 1400}}
        end
      end)

      # Mock all walk calls - use stub to allow any number of calls (code may short-circuit)
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          # CPU sensors
          "1.3.6.1.2.1.25.3.3.1.2" -> {:ok, [%{oid: "1.3.6.1.2.1.25.3.3.1.2.1", value: 45}]}
          # Storage sensors
          "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: "RAM"}]}
          "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: 1000}]}
          "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: 750}]}
          # POE sensors (empty)
          "1.3.6.1.4.1.14988.1.1.15.1.1.3" -> {:error, :no_such_object}
          "1.3.6.1.4.1.14988.1.1.15.1.1.5" -> {:error, :no_such_object}
          "1.3.6.1.4.1.14988.1.1.15.1.1.6" -> {:error, :no_such_object}
          "1.3.6.1.4.1.14988.1.1.15.1.1.7" -> {:error, :no_such_object}
          # Optical sensors (empty)
          "1.3.6.1.4.1.14988.1.1.16.1.1.2" -> {:error, :no_such_object}
          "1.3.6.1.4.1.14988.1.1.16.1.1.3" -> {:error, :no_such_object}
          "1.3.6.1.4.1.14988.1.1.16.1.1.5" -> {:error, :no_such_object}
          "1.3.6.1.4.1.14988.1.1.16.1.1.7" -> {:error, :no_such_object}
          "1.3.6.1.4.1.14988.1.1.16.1.1.8" -> {:error, :no_such_object}
          # Entity sensors from Base
          "1.3.6.1.2.1.99.1.1.1.1" -> {:ok, []}
        end
      end)

      assert {:ok, sensors} = Mikrotik.discover_sensors(@test_opts)

      # Should have temperature sensors
      temp_sensors = Enum.filter(sensors, &String.contains?(&1.sensor_type, "temperature"))
      assert temp_sensors != []

      # Should have voltage sensors
      voltage_sensors = Enum.filter(sensors, &String.contains?(&1.sensor_type, "voltage"))
      assert voltage_sensors != []

      # Should have fan sensors
      fan_sensors = Enum.filter(sensors, &(&1.sensor_type == "fan1"))
      assert fan_sensors != []

      # Check one sensor in detail
      cpu_temp = Enum.find(sensors, &(&1.sensor_type == "cpu_temperature"))
      assert cpu_temp
      assert cpu_temp.sensor_unit == "°C"
      assert cpu_temp.last_value == 55.0
      assert cpu_temp.status == "ok"
    end

    test "discovers CPU load sensors" do
      # Mock health sensors (none available)
      expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)

      # Mock all walk calls
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          # CPU load per core
          "1.3.6.1.2.1.25.3.3.1.2" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.25.3.3.1.2.1", value: 25},
               %{oid: "1.3.6.1.2.1.25.3.3.1.2.2", value: 50},
               %{oid: "1.3.6.1.2.1.25.3.3.1.2.3", value: 75},
               %{oid: "1.3.6.1.2.1.25.3.3.1.2.4", value: 95}
             ]}

          # Storage, POE, optical, entity as empty
          _ ->
            {:error, :no_such_object}
        end
      end)

      assert {:ok, sensors} = Mikrotik.discover_sensors(@test_opts)

      cpu_sensors = Enum.filter(sensors, &(&1.sensor_type == "cpu_load"))
      assert length(cpu_sensors) == 4

      # Check status determination
      cpu1 = Enum.find(sensors, &(&1.sensor_index == "cpu1"))
      assert cpu1.status == "ok"
      assert cpu1.last_value == 25.0

      cpu4 = Enum.find(sensors, &(&1.sensor_index == "cpu4"))
      assert cpu4.status == "critical"
      assert cpu4.last_value == 95.0
    end

    test "discovers storage sensors with proper status" do
      # Mock health sensors (none)
      expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)

      # Mock all walk calls
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          # CPU (empty)
          "1.3.6.1.2.1.25.3.3.1.2" ->
            {:ok, []}

          # Storage sensors
          "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: "Physical memory"},
               %{oid: "1.3.6.1.2.1.25.2.3.1.3.2", value: "/disk1"}
             ]}

          "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: 8192},
               %{oid: "1.3.6.1.2.1.25.2.3.1.5.2", value: 10_000}
             ]}

          "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: 7168},
               %{oid: "1.3.6.1.2.1.25.2.3.1.6.2", value: 9600}
             ]}

          # POE, optical, entity as empty
          _ ->
            {:error, :no_such_object}
        end
      end)

      assert {:ok, sensors} = Mikrotik.discover_sensors(@test_opts)

      memory_sensor = Enum.find(sensors, &(&1.sensor_type == "memory_usage"))
      assert memory_sensor
      assert memory_sensor.status == "warning"
      assert memory_sensor.last_value == 87.5

      disk_sensor = Enum.find(sensors, &(&1.sensor_type == "disk_usage"))
      assert disk_sensor
      assert disk_sensor.status == "critical"
      assert disk_sensor.last_value == 96.0
    end

    test "discovers POE sensors per port" do
      # Mock health, cpu, storage as empty
      expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)

      # Mock all walk calls
      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          # CPU sensors
          "1.3.6.1.2.1.25.3.3.1.2" ->
            {:ok, []}

          # Storage sensors
          "1.3.6.1.2.1.25.2.3.1.3" ->
            {:error, :no_such_object}

          "1.3.6.1.2.1.25.2.3.1.5" ->
            {:error, :no_such_object}

          "1.3.6.1.2.1.25.2.3.1.6" ->
            {:error, :no_such_object}

          # POE sensors
          "1.3.6.1.4.1.14988.1.1.15.1.1.3" ->
            {:ok, [%{oid: "1.3.6.1.4.1.14988.1.1.15.1.1.3.1", value: "ether1"}]}

          "1.3.6.1.4.1.14988.1.1.15.1.1.5" ->
            {:ok, [%{oid: "1.3.6.1.4.1.14988.1.1.15.1.1.5.1", value: 480}]}

          "1.3.6.1.4.1.14988.1.1.15.1.1.6" ->
            {:ok, [%{oid: "1.3.6.1.4.1.14988.1.1.15.1.1.6.1", value: 500}]}

          "1.3.6.1.4.1.14988.1.1.15.1.1.7" ->
            {:ok, [%{oid: "1.3.6.1.4.1.14988.1.1.15.1.1.7.1", value: 240}]}

          # Optical sensors (empty)
          "1.3.6.1.4.1.14988.1.1.16.1.1.2" ->
            {:error, :no_such_object}

          "1.3.6.1.4.1.14988.1.1.16.1.1.3" ->
            {:error, :no_such_object}

          "1.3.6.1.4.1.14988.1.1.16.1.1.5" ->
            {:error, :no_such_object}

          "1.3.6.1.4.1.14988.1.1.16.1.1.7" ->
            {:error, :no_such_object}

          "1.3.6.1.4.1.14988.1.1.16.1.1.8" ->
            {:error, :no_such_object}

          # Entity sensors from Base
          "1.3.6.1.2.1.99.1.1.1.1" ->
            {:ok, []}
        end
      end)

      assert {:ok, sensors} = Mikrotik.discover_sensors(@test_opts)

      poe_sensors = Enum.filter(sensors, &String.contains?(&1.sensor_type, "poe_"))
      assert length(poe_sensors) == 3

      voltage = Enum.find(sensors, &(&1.sensor_type == "poe_voltage"))
      assert voltage.last_value == 48.0
      assert voltage.sensor_unit == "V"
    end
  end

  describe "identify_device/1" do
    test "identifies MikroTik device from sysDescr with model" do
      system_info = %{
        sys_descr: "RouterOS RB5009UG+S+ (stable) 7.13.2",
        sys_name: "router1",
        firmware_version: "7.13.2"
      }

      result = Mikrotik.identify_device(system_info)

      assert result.manufacturer == "MikroTik"
      assert result.model == "RB5009UG+S+"
      assert result.firmware_version == "7.13.2"
    end

    test "identifies generic RouterOS device when model not found" do
      system_info = %{
        sys_descr: "RouterOS software version 7.12",
        sys_name: "router1"
      }

      result = Mikrotik.identify_device(system_info)

      assert result.manufacturer == "MikroTik"
      assert result.model == "RouterOS Device"
    end

    test "extracts firmware from sysDescr when not in system_info" do
      system_info = %{
        sys_descr: "RouterOS RB2011 version 6.49.7"
      }

      result = Mikrotik.identify_device(system_info)

      assert result.manufacturer == "MikroTik"
      assert result.model == "RB2011"
      assert result.firmware_version == "6.49.7"
    end

    property "always returns manufacturer and model" do
      check all(sys_descr <- StreamData.string(:alphanumeric)) do
        system_info = %{sys_descr: sys_descr}

        result = Mikrotik.identify_device(system_info)

        assert Map.has_key?(result, :manufacturer)
        assert Map.has_key?(result, :model)
      end
    end
  end

  describe "sensor status determination" do
    test "CPU status changes with load" do
      expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.25.3.3.1.2" ->
            {:ok,
             [
               %{oid: "1.3.6.1.2.1.25.3.3.1.2.1", value: 60},
               %{oid: "1.3.6.1.2.1.25.3.3.1.2.2", value: 85},
               %{oid: "1.3.6.1.2.1.25.3.3.1.2.3", value: 95}
             ]}

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

      {:ok, sensors} = Mikrotik.discover_sensors(@test_opts)

      cpu1 = Enum.find(sensors, &(&1.sensor_index == "cpu1"))
      cpu2 = Enum.find(sensors, &(&1.sensor_index == "cpu2"))
      cpu3 = Enum.find(sensors, &(&1.sensor_index == "cpu3"))

      assert cpu1.status == "ok"
      assert cpu2.status == "warning"
      assert cpu3.status == "critical"
    end

    test "memory status changes with usage percentage" do
      expect(SnmpMock, :get, 15, fn _, _, _ -> {:error, :no_such_object} end)

      stub(SnmpMock, :walk, fn _, oid, _ ->
        case oid do
          "1.3.6.1.2.1.25.3.3.1.2" -> {:ok, []}
          "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: "memory"}]}
          "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: 1000}]}
          "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: 970}]}
          _ -> {:error, :no_such_object}
        end
      end)

      {:ok, sensors} = Mikrotik.discover_sensors(@test_opts)

      memory = Enum.find(sensors, &(&1.sensor_type == "memory_usage"))
      assert memory.status == "critical"
      assert memory.last_value == 97.0
    end
  end
end
