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

  import Mox

  alias Towerops.Snmp.NeighborDiscovery
  alias Towerops.Snmp.SnmpMock

  setup :verify_on_exit!

  setup do
    # Create mock interface data
    interfaces = [
      %{
        id: "if-uuid-1",
        device_id: "device-uuid-1",
        if_index: 1,
        if_name: "GigabitEthernet0/1"
      },
      %{
        id: "if-uuid-2",
        device_id: "device-uuid-1",
        if_index: 2,
        if_name: "GigabitEthernet0/2"
      }
    ]

    %{interfaces: interfaces}
  end

  describe "discover_neighbors/2" do
    test "handles no neighbors found", %{interfaces: interfaces} do
      # Mock empty responses (3 walks: lldp_rem, lldp_man_addr, cdp)
      expect(SnmpMock, :walk, 3, fn _, _oid, _ ->
        {:ok, []}
      end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)

      assert neighbors == []
    end

    test "handles SNMP errors gracefully", %{interfaces: interfaces} do
      # When LLDP remote table fails, we don't call management address walk
      # So only 2 walks happen: lldp_rem (fails) and cdp (fails)
      expect(SnmpMock, :walk, 2, fn _, _oid, _ ->
        {:error, :timeout}
      end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)

      assert neighbors == []
    end

    test "discovers LLDP neighbors with management addresses", %{interfaces: interfaces} do
      # First walk: LLDP remote table
      expect(SnmpMock, :walk, fn _, oid, _ ->
        if String.starts_with?(oid, "1.0.8802.1.1.2.1.4.1.1") do
          {:ok,
           [
             %{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: "neighbor-switch-1"},
             %{oid: "1.0.8802.1.1.2.1.4.1.1.7.0.1.1", value: "Gi0/1"},
             %{oid: "1.0.8802.1.1.2.1.4.1.1.9.0.1.1", value: "neighbor-switch-1.example.com"}
           ]}
        else
          {:ok, []}
        end
      end)

      # Second walk: LLDP management address table
      expect(SnmpMock, :walk, fn _, oid, _ ->
        if String.starts_with?(oid, "1.0.8802.1.1.2.1.4.2") do
          # Management address is encoded in the OID index
          # OID: base.1.field.time.local_port.rem_index.addr_subtype.addr_len.addr_bytes
          {:ok,
           [
             %{oid: "1.0.8802.1.1.2.1.4.2.1.3.0.1.1.1.4.192.168.1.100", value: 2}
           ]}
        else
          {:ok, []}
        end
      end)

      # Third walk: CDP (empty)
      expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)

      assert length(neighbors) == 1
      [neighbor] = neighbors
      assert neighbor.protocol == "lldp"
      assert neighbor.remote_chassis_id == "neighbor-switch-1"
      assert neighbor.remote_system_name == "neighbor-switch-1.example.com"
      assert neighbor.remote_address == "192.168.1.100"
    end

    test "discovers LLDP neighbors without management addresses", %{interfaces: interfaces} do
      # First walk: LLDP remote table
      expect(SnmpMock, :walk, fn _, _oid, _ ->
        {:ok,
         [
           %{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: "neighbor-switch-2"},
           %{oid: "1.0.8802.1.1.2.1.4.1.1.9.0.1.1", value: "neighbor-2.example.com"}
         ]}
      end)

      # Second walk: LLDP management address table (empty)
      expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      # Third walk: CDP (empty)
      expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)

      assert length(neighbors) == 1
      [neighbor] = neighbors
      assert neighbor.remote_chassis_id == "neighbor-switch-2"
      assert neighbor.remote_address == nil
    end

    test "discovers CDP neighbors with IP addresses", %{interfaces: interfaces} do
      # First walk: LLDP remote table (empty)
      expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      # Second walk: LLDP management address table (empty - but still called)
      expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      # Third walk: CDP cache table
      expect(SnmpMock, :walk, fn _, _oid, _ ->
        {:ok,
         [
           # Address (field 4) - 4 bytes for IPv4: 10.0.0.1
           %{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.4.1.1", value: <<10, 0, 0, 1>>},
           # System Name (field 6)
           %{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.6.1.1", value: "cisco-switch.example.com"},
           # Port ID (field 7)
           %{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.7.1.1", value: "GigabitEthernet1/0/1"},
           # Platform (field 8)
           %{oid: "1.3.6.1.4.1.9.9.23.1.2.1.1.8.1.1", value: "Cisco IOS"}
         ]}
      end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)

      assert length(neighbors) == 1
      [neighbor] = neighbors
      assert neighbor.protocol == "cdp"
      assert neighbor.remote_system_name == "cisco-switch.example.com"
      assert neighbor.remote_address == "10.0.0.1"
      assert neighbor.remote_platform == "Cisco IOS"
    end

    test "discovers multiple LLDP neighbors with different management addresses", %{
      interfaces: interfaces
    } do
      # First walk: LLDP remote table
      expect(SnmpMock, :walk, fn _, _oid, _ ->
        {:ok,
         [
           # Neighbor 1 on port 1
           %{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: "switch-1"},
           %{oid: "1.0.8802.1.1.2.1.4.1.1.9.0.1.1", value: "switch-1.local"},
           # Neighbor 2 on port 2
           %{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.2.1", value: "switch-2"},
           %{oid: "1.0.8802.1.1.2.1.4.1.1.9.0.2.1", value: "switch-2.local"}
         ]}
      end)

      # Second walk: LLDP management address table
      expect(SnmpMock, :walk, fn _, _oid, _ ->
        {:ok,
         [
           # IPv4 192.168.1.10 for neighbor on port 1
           %{oid: "1.0.8802.1.1.2.1.4.2.1.3.0.1.1.1.4.192.168.1.10", value: 2},
           # IPv4 192.168.1.20 for neighbor on port 2
           %{oid: "1.0.8802.1.1.2.1.4.2.1.3.0.2.1.1.4.192.168.1.20", value: 2}
         ]}
      end)

      # Third walk: CDP (empty)
      expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)

      assert length(neighbors) == 2

      neighbor_1 = Enum.find(neighbors, &(&1.remote_chassis_id == "switch-1"))
      neighbor_2 = Enum.find(neighbors, &(&1.remote_chassis_id == "switch-2"))

      assert neighbor_1.remote_address == "192.168.1.10"
      assert neighbor_2.remote_address == "192.168.1.20"
    end

    test "handles IPv6 management addresses", %{interfaces: interfaces} do
      # First walk: LLDP remote table
      expect(SnmpMock, :walk, fn _, _oid, _ ->
        {:ok,
         [
           %{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: "ipv6-switch"},
           %{oid: "1.0.8802.1.1.2.1.4.1.1.9.0.1.1", value: "ipv6-switch.local"}
         ]}
      end)

      # Second walk: LLDP management address table with IPv6
      expect(SnmpMock, :walk, fn _, _oid, _ ->
        # IPv6 address 2001:db8::1
        # addr_subtype=2 (IPv6), addr_len=16
        # 2001:0db8:0000:0000:0000:0000:0000:0001
        {:ok,
         [
           %{oid: "1.0.8802.1.1.2.1.4.2.1.3.0.1.1.2.16.32.1.13.184.0.0.0.0.0.0.0.0.0.0.0.1", value: 2}
         ]}
      end)

      # Third walk: CDP (empty)
      expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)

      assert length(neighbors) == 1
      [neighbor] = neighbors
      assert neighbor.remote_chassis_id == "ipv6-switch"
      # IPv6 address formatted
      assert neighbor.remote_address == "2001:DB8:0:0:0:0:0:1"
    end

    test "handles malformed management address OIDs gracefully", %{interfaces: interfaces} do
      # First walk: LLDP remote table
      expect(SnmpMock, :walk, fn _, _oid, _ ->
        {:ok,
         [
           %{oid: "1.0.8802.1.1.2.1.4.1.1.5.0.1.1", value: "switch-malformed"},
           %{oid: "1.0.8802.1.1.2.1.4.1.1.9.0.1.1", value: "switch-malformed.local"}
         ]}
      end)

      # Second walk: LLDP management address table with malformed entry
      expect(SnmpMock, :walk, fn _, _oid, _ ->
        # Malformed OID - not enough parts
        {:ok,
         [
           %{oid: "1.0.8802.1.1.2.1.4.2.1.3.0.1", value: 2}
         ]}
      end)

      # Third walk: CDP (empty)
      expect(SnmpMock, :walk, fn _, _, _ -> {:ok, []} end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, neighbors} = NeighborDiscovery.discover_neighbors(client_opts, interfaces)

      assert length(neighbors) == 1
      [neighbor] = neighbors
      # No management address parsed due to malformed OID
      assert neighbor.remote_address == nil
    end
  end
end
