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

  import Mox

  alias Towerops.Snmp.Profiles.Cisco
  alias Towerops.Snmp.SnmpMock

  setup :verify_on_exit!

  @client_opts [ip: "192.168.1.1", community: "public", version: :v2c, port: 161]

  describe "discover_system_info/1" do
    test "delegates to Base profile" do
      # Mock the SNMP calls that Base.discover_system_info makes
      # Base profile queries 6 system OIDs - match by OID string
      expect(SnmpMock, :get, 6, fn _target, oid, _opts ->
        case oid do
          # sysDescr
          "1.3.6.1.2.1.1.1.0" -> {:ok, "Cisco IOS Software"}
          # sysObjectID
          "1.3.6.1.2.1.1.2.0" -> {:ok, [1, 3, 6, 1, 4, 1, 9]}
          # sysUpTime
          "1.3.6.1.2.1.1.3.0" -> {:ok, 12_345}
          # sysContact
          "1.3.6.1.2.1.1.4.0" -> {:ok, "admin@example.com"}
          # sysName
          "1.3.6.1.2.1.1.5.0" -> {:ok, "test-device"}
          # sysLocation
          "1.3.6.1.2.1.1.6.0" -> {:ok, "Test Location"}
          _ -> {:error, :unknown_oid}
        end
      end)

      result = Cisco.discover_system_info(@client_opts)

      # Should delegate to Base and return its result
      assert {:ok, system_info} = result
      assert is_map(system_info)
      assert system_info.sys_name == "test-device"
    end
  end

  describe "discover_interfaces/1" do
    test "delegates to Base profile" do
      # Mock SNMP walk for interfaces - return empty list to indicate no interfaces found
      # Note: snmp_adapter().walk returns {:ok, [%{oid: ..., value: ...}]}
      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, []}
      end)

      result = Cisco.discover_interfaces(@client_opts)

      # Should delegate to Base and return empty list when no interfaces found
      assert {:ok, []} = result
    end
  end

  describe "discover_sensors/1" do
    test "attempts Cisco-specific sensor discovery" do
      # Mock walk for Cisco sensors - return empty list to indicate no Cisco sensors
      # Note: snmp_adapter().walk returns {:ok, [%{oid: ..., value: ...}]}
      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, []}
      end)

      # Should fall back to Base sensors when Cisco sensors not found
      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, []}
      end)

      result = Cisco.discover_sensors(@client_opts)

      # Should return empty list when no sensors found
      assert {:ok, []} = result
    end

    test "delegates to Base profile for sensor discovery" do
      # When Cisco-specific sensors fail, should delegate to Base
      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, []}
      end)

      # Base profile walk attempt
      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, []}
      end)

      result = Cisco.discover_sensors(@client_opts)

      # Should return empty list when no sensors found
      assert {:ok, []} = result
    end
  end

  describe "identify_device/1" do
    test "identifies Catalyst switch model" do
      system_info = %{
        sys_descr: "Cisco IOS Software, C2960 Software (C2960-LANBASEK9-M), Version 15.2(4)E7, RELEASE SOFTWARE",
        sys_name: "switch1"
      }

      device_info = Cisco.identify_device(system_info)

      assert device_info.manufacturer == "Cisco"
      assert device_info.model == "C2960"
      assert device_info.firmware_version == "15.2(4)E7"
    end

    test "identifies WS-C switch model" do
      system_info = %{
        sys_descr: "Cisco IOS Software, WS-C3750X Software...",
        sys_name: "switch2"
      }

      device_info = Cisco.identify_device(system_info)

      assert device_info.manufacturer == "Cisco"
      assert device_info.model == "WS-C3750X"
    end

    test "identifies ISR router model" do
      system_info = %{
        sys_descr: "Cisco IOS Software, ISR4451 Software...",
        sys_name: "router1"
      }

      device_info = Cisco.identify_device(system_info)

      assert device_info.manufacturer == "Cisco"
      assert device_info.model == "ISR4451"
    end

    test "identifies numeric router model with C prefix" do
      system_info = %{
        sys_descr: "Cisco IOS Software, 1900 Software (C1900-UNIVERSALK9-M)...",
        sys_name: "router2"
      }

      device_info = Cisco.identify_device(system_info)

      assert device_info.manufacturer == "Cisco"
      # C1900 matches the Catalyst pattern first
      assert device_info.model == "C1900"
    end

    test "identifies ASR router model" do
      system_info = %{
        sys_descr: "Cisco IOS XE Software, ASR1000 Software...",
        sys_name: "router3"
      }

      device_info = Cisco.identify_device(system_info)

      assert device_info.manufacturer == "Cisco"
      assert device_info.model == "ASR1000"
    end

    test "returns unknown model for unrecognized format" do
      system_info = %{
        sys_descr: "Some unknown Cisco device",
        sys_name: "unknown"
      }

      device_info = Cisco.identify_device(system_info)

      assert device_info.manufacturer == "Cisco"
      assert device_info.model == "Unknown Cisco Model"
    end

    test "extracts firmware version" do
      system_info = %{
        sys_descr: "Cisco IOS Software, Version 15.2(4)E7, RELEASE SOFTWARE",
        sys_name: "switch"
      }

      device_info = Cisco.identify_device(system_info)

      assert device_info.firmware_version == "15.2(4)E7"
    end

    test "returns nil for firmware when not found" do
      system_info = %{
        sys_descr: "Cisco device without version info",
        sys_name: "device"
      }

      device_info = Cisco.identify_device(system_info)

      assert is_nil(device_info.firmware_version)
    end
  end

  describe "sensor type parsing" do
    test "attempts to discover Cisco-specific sensors" do
      # Mock walk to indicate Cisco sensors available
      # Note: snmp_adapter().walk returns {:ok, [%{oid: ..., value: ...}]}
      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, [%{oid: "1.3.6.1.4.1.9.9.91.1.1.1.1.1.1000", value: 8}]}
      end)

      # Mock individual get calls that get_multiple makes
      # For each sensor, we need 7 OID calls (type, scale, precision, value, status, descr, name)
      expect(SnmpMock, :get, 7, fn _, _, _ ->
        # Return sample values for all 7 OIDs
        {:ok, 8}
      end)

      result = Cisco.discover_sensors(@client_opts)

      # Should attempt discovery and return sensors
      assert {:ok, sensors} = result
      assert is_list(sensors)
    end

    test "falls back to Base when Cisco sensors fail" do
      # Mock walk returning empty list for Cisco-specific
      expect(SnmpMock, :walk, 2, fn _, _, _ ->
        {:ok, []}
      end)

      result = Cisco.discover_sensors(@client_opts)

      # Should fall back to Base profile
      assert {:ok, []} = result
    end

    test "discovers various sensor types" do
      # Test different sensor types to cover more parsing functions
      for sensor_type <- [1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 99] do
        expect(SnmpMock, :walk, fn _, _, _ ->
          {:ok, [%{oid: "1.3.6.1.4.1.9.9.91.1.1.1.1.1.#{sensor_type}", value: sensor_type}]}
        end)

        # Mock get_multiple to return proper sensor data
        expect(SnmpMock, :get, 7, fn _, oid, _ ->
          cond do
            String.contains?(oid, ".1.1.1.1.1.") -> {:ok, sensor_type}
            String.contains?(oid, ".1.1.1.1.2.") -> {:ok, -3}
            String.contains?(oid, ".1.1.1.1.3.") -> {:ok, 1}
            String.contains?(oid, ".1.1.1.1.4.") -> {:ok, 1000}
            String.contains?(oid, ".1.1.1.1.5.") -> {:ok, 1}
            String.contains?(oid, ".1.1.1.1.2.") -> {:ok, "Test Sensor"}
            true -> {:ok, ""}
          end
        end)

        result = Cisco.discover_sensors(@client_opts)
        assert {:ok, _sensors} = result
      end
    end

    test "handles different sensor scales" do
      for scale <- [-24, -12, -9, -6, -3, 0, 3, 5] do
        expect(SnmpMock, :walk, fn _, _, _ ->
          {:ok, [%{oid: "1.3.6.1.4.1.9.9.91.1.1.1.1.1.1000", value: 8}]}
        end)

        expect(SnmpMock, :get, 7, fn _, oid, _ ->
          cond do
            String.contains?(oid, ".1.1.1.1.1.") -> {:ok, 8}
            String.contains?(oid, ".1.1.1.1.2.") -> {:ok, scale}
            String.contains?(oid, ".1.1.1.1.3.") -> {:ok, 1}
            String.contains?(oid, ".1.1.1.1.4.") -> {:ok, 1000}
            String.contains?(oid, ".1.1.1.1.5.") -> {:ok, 1}
            true -> {:ok, "Test"}
          end
        end)

        result = Cisco.discover_sensors(@client_opts)
        assert {:ok, _sensors} = result
      end
    end

    test "handles different sensor statuses" do
      for status <- [1, 2, 3, 99] do
        expect(SnmpMock, :walk, fn _, _, _ ->
          {:ok, [%{oid: "1.3.6.1.4.1.9.9.91.1.1.1.1.1.1000", value: 8}]}
        end)

        expect(SnmpMock, :get, 7, fn _, oid, _ ->
          cond do
            String.contains?(oid, ".1.1.1.1.1.") -> {:ok, 8}
            String.contains?(oid, ".1.1.1.1.2.") -> {:ok, 0}
            String.contains?(oid, ".1.1.1.1.3.") -> {:ok, 1}
            String.contains?(oid, ".1.1.1.1.4.") -> {:ok, 1000}
            String.contains?(oid, ".1.1.1.1.5.") -> {:ok, status}
            true -> {:ok, "Test"}
          end
        end)

        result = Cisco.discover_sensors(@client_opts)
        assert {:ok, _sensors} = result
      end
    end

    test "handles sensor with only name (no descr)" do
      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, [%{oid: "1.3.6.1.4.1.9.9.91.1.1.1.1.1.1000", value: 8}]}
      end)

      expect(SnmpMock, :get, 7, fn _, oid, _ ->
        cond do
          String.contains?(oid, ".1.1.1.1.1.") -> {:ok, 8}
          String.contains?(oid, ".1.1.1.1.2.") -> {:ok, 0}
          String.contains?(oid, ".1.1.1.1.3.") -> {:ok, 1}
          String.contains?(oid, ".1.1.1.1.4.") -> {:ok, 1000}
          String.contains?(oid, ".1.1.1.1.5.") -> {:ok, 1}
          String.contains?(oid, ".1.1.1.1.2.") -> {:ok, ""}
          true -> {:ok, "Sensor Name"}
        end
      end)

      result = Cisco.discover_sensors(@client_opts)
      assert {:ok, _sensors} = result
    end

    test "handles sensor get_multiple failure" do
      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, [%{oid: "1.3.6.1.4.1.9.9.91.1.1.1.1.1.1000", value: 8}]}
      end)

      # Mock get_multiple to fail
      expect(SnmpMock, :get, 7, fn _, _, _ ->
        {:error, :timeout}
      end)

      # Falls back to Base.discover_sensors which calls walk again
      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, []}
      end)

      result = Cisco.discover_sensors(@client_opts)

      # Should return empty list when sensor building fails
      assert {:ok, []} = result
    end

    test "handles non-integer scale in calculate_divisor" do
      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, [%{oid: "1.3.6.1.4.1.9.9.91.1.1.1.1.1.1000", value: 8}]}
      end)

      expect(SnmpMock, :get, 7, fn _, oid, _ ->
        cond do
          String.contains?(oid, ".1.1.1.1.1.") -> {:ok, 8}
          String.contains?(oid, ".1.1.1.1.2.") -> {:ok, "invalid"}
          String.contains?(oid, ".1.1.1.1.3.") -> {:ok, 1}
          String.contains?(oid, ".1.1.1.1.4.") -> {:ok, 1000}
          String.contains?(oid, ".1.1.1.1.5.") -> {:ok, 1}
          true -> {:ok, "Test"}
        end
      end)

      result = Cisco.discover_sensors(@client_opts)
      assert {:ok, _sensors} = result
    end

    test "handles invalid sensor value" do
      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, [%{oid: "1.3.6.1.4.1.9.9.91.1.1.1.1.1.1000", value: 8}]}
      end)

      expect(SnmpMock, :get, 7, fn _, oid, _ ->
        cond do
          String.contains?(oid, ".1.1.1.1.1.") -> {:ok, 8}
          String.contains?(oid, ".1.1.1.1.2.") -> {:ok, 0}
          String.contains?(oid, ".1.1.1.1.3.") -> {:ok, 1}
          String.contains?(oid, ".1.1.1.1.4.") -> {:ok, "invalid"}
          String.contains?(oid, ".1.1.1.1.5.") -> {:ok, 1}
          true -> {:ok, "Test"}
        end
      end)

      result = Cisco.discover_sensors(@client_opts)
      assert {:ok, sensors} = result
      # Should still return sensor but with nil value
      assert length(sensors) == 1
    end
  end
end
