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

  import Mox

  alias Towerops.Snmp.ArpDiscovery
  alias Towerops.Snmp.SnmpMock

  setup :verify_on_exit!

  describe "discover_arp_table/1" do
    test "returns empty list when no ARP entries found" do
      # Mock empty responses for both walks (phys_address and type)
      expect(SnmpMock, :walk, 2, fn _, _oid, _ ->
        {:ok, []}
      end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, arp_entries} = ArpDiscovery.discover_arp_table(client_opts)

      assert arp_entries == []
    end

    test "handles SNMP errors gracefully" do
      # Mock error responses for both walks
      expect(SnmpMock, :walk, 2, fn _, _oid, _ ->
        {:error, :timeout}
      end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, arp_entries} = ArpDiscovery.discover_arp_table(client_opts)

      assert arp_entries == []
    end

    test "discovers ARP entries with dynamic type" do
      # First walk: ipNetToMediaPhysAddress (MAC addresses)
      expect(SnmpMock, :walk, fn _, oid, _ ->
        if String.starts_with?(oid, "1.3.6.1.2.1.4.22.1.2") do
          # OID format: base.if_index.ip_octet1.ip_octet2.ip_octet3.ip_octet4
          {:ok,
           [
             %{
               oid: "1.3.6.1.2.1.4.22.1.2.1.192.168.1.100",
               value: <<0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF>>
             },
             %{
               oid: "1.3.6.1.2.1.4.22.1.2.2.10.0.0.50",
               value: <<0x11, 0x22, 0x33, 0x44, 0x55, 0x66>>
             }
           ]}
        else
          {:ok, []}
        end
      end)

      # Second walk: ipNetToMediaType (entry types)
      expect(SnmpMock, :walk, fn _, oid, _ ->
        if String.starts_with?(oid, "1.3.6.1.2.1.4.22.1.4") do
          {:ok,
           [
             %{oid: "1.3.6.1.2.1.4.22.1.4.1.192.168.1.100", value: 3},
             %{oid: "1.3.6.1.2.1.4.22.1.4.2.10.0.0.50", value: 3}
           ]}
        else
          {:ok, []}
        end
      end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, arp_entries} = ArpDiscovery.discover_arp_table(client_opts)

      assert length(arp_entries) == 2

      entry1 = Enum.find(arp_entries, &(&1.ip_address == "192.168.1.100"))
      assert entry1.mac_address == "aa:bb:cc:dd:ee:ff"
      assert entry1.if_index == 1
      assert entry1.entry_type == "dynamic"

      entry2 = Enum.find(arp_entries, &(&1.ip_address == "10.0.0.50"))
      assert entry2.mac_address == "11:22:33:44:55:66"
      assert entry2.if_index == 2
      assert entry2.entry_type == "dynamic"
    end

    test "discovers ARP entries with static type" do
      # First walk: MAC addresses
      expect(SnmpMock, :walk, fn _, oid, _ ->
        if String.starts_with?(oid, "1.3.6.1.2.1.4.22.1.2") do
          {:ok,
           [
             %{
               oid: "1.3.6.1.2.1.4.22.1.2.1.192.168.1.1",
               value: <<0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01>>
             }
           ]}
        else
          {:ok, []}
        end
      end)

      # Second walk: entry types (4 = static)
      expect(SnmpMock, :walk, fn _, oid, _ ->
        if String.starts_with?(oid, "1.3.6.1.2.1.4.22.1.4") do
          {:ok,
           [
             %{oid: "1.3.6.1.2.1.4.22.1.4.1.192.168.1.1", value: 4}
           ]}
        else
          {:ok, []}
        end
      end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, arp_entries} = ArpDiscovery.discover_arp_table(client_opts)

      assert length(arp_entries) == 1

      [entry] = arp_entries
      assert entry.ip_address == "192.168.1.1"
      assert entry.mac_address == "de:ad:be:ef:00:01"
      assert entry.if_index == 1
      assert entry.entry_type == "static"
    end

    test "handles entries without type information" do
      # First walk: MAC addresses
      expect(SnmpMock, :walk, fn _, oid, _ ->
        if String.starts_with?(oid, "1.3.6.1.2.1.4.22.1.2") do
          {:ok,
           [
             %{
               oid: "1.3.6.1.2.1.4.22.1.2.5.172.16.0.1",
               value: <<0x00, 0x11, 0x22, 0x33, 0x44, 0x55>>
             }
           ]}
        else
          {:ok, []}
        end
      end)

      # Second walk: empty (no type information)
      expect(SnmpMock, :walk, fn _, _, _ ->
        {:ok, []}
      end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, arp_entries} = ArpDiscovery.discover_arp_table(client_opts)

      assert length(arp_entries) == 1

      [entry] = arp_entries
      assert entry.ip_address == "172.16.0.1"
      assert entry.mac_address == "00:11:22:33:44:55"
      assert entry.if_index == 5
      assert entry.entry_type == nil
    end

    test "handles all entry types" do
      # First walk: MAC addresses
      expect(SnmpMock, :walk, fn _, oid, _ ->
        if String.starts_with?(oid, "1.3.6.1.2.1.4.22.1.2") do
          {:ok,
           [
             %{oid: "1.3.6.1.2.1.4.22.1.2.1.192.168.1.1", value: <<0x01, 0x02, 0x03, 0x04, 0x05, 0x06>>},
             %{oid: "1.3.6.1.2.1.4.22.1.2.1.192.168.1.2", value: <<0x01, 0x02, 0x03, 0x04, 0x05, 0x07>>},
             %{oid: "1.3.6.1.2.1.4.22.1.2.1.192.168.1.3", value: <<0x01, 0x02, 0x03, 0x04, 0x05, 0x08>>},
             %{oid: "1.3.6.1.2.1.4.22.1.2.1.192.168.1.4", value: <<0x01, 0x02, 0x03, 0x04, 0x05, 0x09>>}
           ]}
        else
          {:ok, []}
        end
      end)

      # Second walk: entry types
      # 1 = other, 2 = invalid, 3 = dynamic, 4 = static
      expect(SnmpMock, :walk, fn _, oid, _ ->
        if String.starts_with?(oid, "1.3.6.1.2.1.4.22.1.4") do
          {:ok,
           [
             %{oid: "1.3.6.1.2.1.4.22.1.4.1.192.168.1.1", value: 1},
             %{oid: "1.3.6.1.2.1.4.22.1.4.1.192.168.1.2", value: 2},
             %{oid: "1.3.6.1.2.1.4.22.1.4.1.192.168.1.3", value: 3},
             %{oid: "1.3.6.1.2.1.4.22.1.4.1.192.168.1.4", value: 4}
           ]}
        else
          {:ok, []}
        end
      end)

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, arp_entries} = ArpDiscovery.discover_arp_table(client_opts)

      assert length(arp_entries) == 4

      types_by_ip = Map.new(arp_entries, &{&1.ip_address, &1.entry_type})

      assert types_by_ip["192.168.1.1"] == "other"
      assert types_by_ip["192.168.1.2"] == "invalid"
      assert types_by_ip["192.168.1.3"] == "dynamic"
      assert types_by_ip["192.168.1.4"] == "static"
    end

    test "includes last_seen_at timestamp" do
      now = DateTime.utc_now()

      # First walk: MAC addresses
      expect(SnmpMock, :walk, fn _, oid, _ ->
        if String.starts_with?(oid, "1.3.6.1.2.1.4.22.1.2") do
          {:ok,
           [
             %{oid: "1.3.6.1.2.1.4.22.1.2.1.10.0.0.1", value: <<0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF>>}
           ]}
        else
          {:ok, []}
        end
      end)

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

      client_opts = [ip: "192.168.1.1", community: "public", version: "2c"]
      {:ok, arp_entries} = ArpDiscovery.discover_arp_table(client_opts)

      assert length(arp_entries) == 1
      [entry] = arp_entries

      assert %DateTime{} = entry.last_seen_at
      # Timestamp should be within 1 second of now
      assert DateTime.diff(entry.last_seen_at, now, :second) <= 1
    end
  end
end
