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 (8 OIDs per interface × 3 interfaces = 24 calls)
      expect(SnmpMock, :get, 24, 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 == "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 == "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 == "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
      expect(SnmpMock, :walk, fn _, "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}}
         ]}
      end)

      # Mock sensor data for each sensor (4 OIDs per sensor × 2 sensors = 8 calls)
      expect(SnmpMock, :get, 8, fn _, oid, _ ->
        cond do
          String.ends_with?(oid, ".1") ->
            case oid |> String.split(".") |> Enum.at(-2) do
              "1" -> {:ok, {:integer, 8}}
              "2" -> {: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, -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.sensor_divisor == 1
      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.sensor_divisor == 1_000
      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
      expect(SnmpMock, :walk, fn _, "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}}
         ]}
      end)

      # First sensor succeeds, second fails
      expect(SnmpMock, :get, 8, fn _, oid, _ ->
        if String.ends_with?(oid, ".1") do
          case oid |> String.split(".") |> Enum.at(-2) do
            "1" -> {:ok, {:integer, 8}}
            "2" -> {: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"
      assert device_info.model == "Server"
    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"
      assert device_info.model == "Windows Server"
    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
  end
end
