defmodule SnmpKit.SnmpLib.OIDTest do
  use ExUnit.Case, async: true

  alias SnmpKit.SnmpLib.OID

  @moduletag :unit
  @moduletag :protocol
  @moduletag :phase_2

  describe "String/List conversions" do
    test "converts valid OID string to list" do
      {:ok, oid_list} = OID.string_to_list("1.3.6.1.2.1.1.1.0")
      assert oid_list == [1, 3, 6, 1, 2, 1, 1, 1, 0]
    end

    test "converts valid OID string with leading dot to list" do
      {:ok, oid_list} = OID.string_to_list(".1.3.6.1.2.1.1.1.0")
      assert oid_list == [1, 3, 6, 1, 2, 1, 1, 1, 0]
    end

    test "converts valid OID list to string" do
      {:ok, oid_string} = OID.list_to_string([1, 3, 6, 1, 2, 1, 1, 1, 0])
      assert oid_string == "1.3.6.1.2.1.1.1.0"
    end

    test "handles single component OID" do
      {:ok, oid_list} = OID.string_to_list("42")
      assert oid_list == [42]

      {:ok, oid_string} = OID.list_to_string([42])
      assert oid_string == "42"
    end

    test "handles single component OID with leading dot" do
      {:ok, oid_list} = OID.string_to_list(".42")
      assert oid_list == [42]
    end

    test "handles short OID with leading dot" do
      {:ok, oid_list} = OID.string_to_list(".1.3.6")
      assert oid_list == [1, 3, 6]
    end

    test "rejects empty OID string" do
      assert {:error, :empty_oid} = OID.string_to_list("")
      assert {:error, :empty_oid} = OID.string_to_list("   ")
      assert {:error, :empty_oid} = OID.string_to_list(".")
      assert {:error, :empty_oid} = OID.string_to_list("  .  ")
    end

    test "rejects empty OID list" do
      assert {:error, :empty_oid} = OID.list_to_string([])
    end

    test "rejects invalid OID string formats" do
      assert {:error, :invalid_oid_string} = OID.string_to_list("1.3.6.1.a.2")
      assert {:error, :invalid_oid_string} = OID.string_to_list("1..3.6.1")
      assert {:error, :invalid_oid_string} = OID.string_to_list("1.3.6.1.")
      assert {:error, :invalid_oid_string} = OID.string_to_list(".1.3.6.1.a.2")
      assert {:error, :invalid_oid_string} = OID.string_to_list(".1..3.6.1")
      assert {:error, :invalid_oid_string} = OID.string_to_list(".1.3.6.1.")
    end

    test "rejects negative components" do
      assert {:error, :invalid_component} = OID.list_to_string([1, 3, -1, 4])
      assert {:error, :invalid_component} = OID.list_to_string([-1])
    end

    test "rejects non-integer components" do
      assert {:error, :invalid_component} = OID.list_to_string([1, 3, "6", 1])
      assert {:error, :invalid_component} = OID.list_to_string([1.5, 3, 6])
    end

    test "handles large OID components" do
      large_oid = [1, 3, 6, 1, 4, 1, 999_999, 1, 2, 3]
      {:ok, oid_string} = OID.list_to_string(large_oid)
      {:ok, parsed_oid} = OID.string_to_list(oid_string)
      assert parsed_oid == large_oid
    end
  end

  describe "Tree operations" do
    test "correctly identifies child relationships" do
      parent = [1, 3, 6, 1, 2, 1]
      child = [1, 3, 6, 1, 2, 1, 1, 1, 0]

      assert OID.child_of?(child, parent) == true
      assert OID.child_of?(parent, child) == false
    end

    test "correctly identifies parent relationships" do
      parent = [1, 3, 6, 1, 2, 1]
      child = [1, 3, 6, 1, 2, 1, 1, 1, 0]

      assert OID.parent_of?(parent, child) == true
      assert OID.parent_of?(child, parent) == false
    end

    test "rejects equal OIDs as child/parent" do
      oid = [1, 3, 6, 1, 2, 1]

      assert OID.child_of?(oid, oid) == false
      assert OID.parent_of?(oid, oid) == false
    end

    test "rejects sibling OIDs as child/parent" do
      oid1 = [1, 3, 6, 1, 2, 1]
      oid2 = [1, 3, 6, 1, 2, 2]

      assert OID.child_of?(oid1, oid2) == false
      assert OID.child_of?(oid2, oid1) == false
    end

    test "gets parent OID correctly" do
      oid = [1, 3, 6, 1, 2, 1, 1, 1, 0]
      {:ok, parent} = OID.get_parent(oid)
      assert parent == [1, 3, 6, 1, 2, 1, 1, 1]
    end

    test "handles root OIDs in get_parent" do
      assert {:error, :root_oid} = OID.get_parent([])
      assert {:error, :root_oid} = OID.get_parent([1])
    end

    test "gets immediate children from OID set" do
      parent = [1, 3, 6, 1]

      oid_set = [
        [1, 3, 6, 1, 2],
        [1, 3, 6, 1, 4],
        [1, 3, 6, 1, 2, 1],
        [1, 3, 6, 1, 4, 1, 9],
        [1, 3, 6, 2]
      ]

      children = OID.get_children(parent, oid_set)
      assert length(children) == 2
      assert [1, 3, 6, 1, 2] in children
      assert [1, 3, 6, 1, 4] in children
    end

    test "finds next OID in sorted set" do
      current = [1, 3, 6, 1, 2, 1, 1, 1, 0]

      oid_set = [
        [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]
      ]

      {:ok, next_oid} = OID.get_next_oid(current, oid_set)
      assert next_oid == [1, 3, 6, 1, 2, 1, 1, 2, 0]
    end

    test "handles end of MIB in get_next_oid" do
      current = [1, 3, 6, 1, 2, 1, 1, 3, 0]

      oid_set = [
        [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]
      ]

      assert {:error, :end_of_mib} = OID.get_next_oid(current, oid_set)
    end
  end

  describe "Comparison operations" do
    test "compares OIDs lexicographically" do
      oid1 = [1, 3, 6, 1]
      oid2 = [1, 3, 6, 2]
      oid3 = [1, 3, 6, 1]
      oid4 = [1, 3, 6, 1, 2]

      assert OID.compare(oid1, oid2) == :lt
      assert OID.compare(oid2, oid1) == :gt
      assert OID.compare(oid1, oid3) == :eq
      assert OID.compare(oid1, oid4) == :lt
      assert OID.compare(oid4, oid1) == :gt
    end

    test "sorts OID list correctly" do
      oids = [
        [1, 3, 6, 2],
        [1, 3, 6, 1, 2],
        [1, 3, 6, 1],
        [1, 3, 6, 1, 4, 1],
        [1, 3, 6, 1, 1]
      ]

      sorted = OID.sort(oids)

      expected = [
        [1, 3, 6, 1],
        [1, 3, 6, 1, 1],
        [1, 3, 6, 1, 2],
        [1, 3, 6, 1, 4, 1],
        [1, 3, 6, 2]
      ]

      assert sorted == expected
    end

    test "handles empty list in sort" do
      assert OID.sort([]) == []
      assert OID.sort(:not_a_list) == []
    end
  end

  describe "Table operations" do
    test "extracts table index correctly" do
      table_oid = [1, 3, 6, 1, 2, 1, 2, 2, 1, 1]
      instance_oid = [1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1]

      {:ok, index} = OID.extract_table_index(table_oid, instance_oid)
      assert index == [1]
    end

    test "extracts complex table index" do
      table_oid = [1, 3, 6, 1, 2, 1, 4, 20, 1, 1]
      instance_oid = [1, 3, 6, 1, 2, 1, 4, 20, 1, 1, 192, 168, 1, 1]

      {:ok, index} = OID.extract_table_index(table_oid, instance_oid)
      assert index == [192, 168, 1, 1]
    end

    test "rejects invalid table instances" do
      table_oid = [1, 3, 6, 1, 2, 1, 2, 2, 1, 1]
      # Wrong table column
      invalid_instance = [1, 3, 6, 1, 2, 1, 2, 2, 1, 2, 1]

      assert {:error, :invalid_table_instance} =
               OID.extract_table_index(table_oid, invalid_instance)
    end

    test "builds table instance from OID and index" do
      table_oid = [1, 3, 6, 1, 2, 1, 2, 2, 1, 1]
      index = [1]

      {:ok, instance_oid} = OID.build_table_instance(table_oid, index)
      assert instance_oid == [1, 3, 6, 1, 2, 1, 2, 2, 1, 1, 1]
    end

    test "builds complex table instance" do
      table_oid = [1, 3, 6, 1, 2, 1, 4, 20, 1, 1]
      index = [192, 168, 1, 1]

      {:ok, instance_oid} = OID.build_table_instance(table_oid, index)
      assert instance_oid == [1, 3, 6, 1, 2, 1, 4, 20, 1, 1, 192, 168, 1, 1]
    end

    test "parses integer table index" do
      {:ok, value} = OID.parse_table_index([42], :integer)
      assert value == 42
    end

    test "parses fixed-length string table index" do
      # "test" in ASCII
      index = [116, 101, 115, 116]
      {:ok, value} = OID.parse_table_index(index, {:string, 4})
      assert value == "test"
    end

    test "parses variable-length string table index" do
      # Length-prefixed "test"
      index = [4, 116, 101, 115, 116]
      {:ok, value} = OID.parse_table_index(index, {:variable_string})
      assert value == "test"
    end

    test "builds integer table index" do
      {:ok, index} = OID.build_table_index(42, :integer)
      assert index == [42]
    end

    test "builds fixed-length string table index" do
      {:ok, index} = OID.build_table_index("test", {:string, 4})
      assert index == [116, 101, 115, 116]
    end

    test "builds variable-length string table index" do
      {:ok, index} = OID.build_table_index("test", {:variable_string})
      assert index == [4, 116, 101, 115, 116]
    end

    test "rejects invalid string length for fixed-length index" do
      assert {:error, :invalid_string_length} = OID.build_table_index("test", {:string, 5})
    end
  end

  describe "Validation" do
    test "validates correct OIDs" do
      assert :ok = OID.valid_oid?([1, 3, 6, 1, 2, 1, 1, 1, 0])
      assert :ok = OID.valid_oid?([0])
      assert :ok = OID.valid_oid?([1, 2, 3, 4, 5])
    end

    test "rejects invalid OIDs" do
      assert {:error, :empty_oid} = OID.valid_oid?([])
      assert {:error, :invalid_component} = OID.valid_oid?([1, 3, -1, 4])
      assert {:error, :invalid_component} = OID.valid_oid?([1, 3, "6", 1])
      assert {:error, :invalid_input} = OID.valid_oid?(:not_a_list)
    end

    test "normalizes different OID formats" do
      {:ok, normalized1} = OID.normalize([1, 3, 6, 1])
      assert normalized1 == [1, 3, 6, 1]

      {:ok, normalized2} = OID.normalize("1.3.6.1")
      assert normalized2 == [1, 3, 6, 1]

      assert {:error, :invalid_input} = OID.normalize(:invalid)
    end
  end

  describe "Standard OID utilities" do
    test "returns standard OID prefixes" do
      assert OID.mib_2() == [1, 3, 6, 1, 2, 1]
      assert OID.enterprises() == [1, 3, 6, 1, 4, 1]
      assert OID.experimental() == [1, 3, 6, 1, 3]
      assert OID.private() == [1, 3, 6, 1, 4]
    end

    test "identifies MIB-2 OIDs" do
      assert OID.mib_2?([1, 3, 6, 1, 2, 1]) == true
      assert OID.mib_2?([1, 3, 6, 1, 2, 1, 1, 1, 0]) == true
      assert OID.mib_2?([1, 3, 6, 1, 4, 1, 9]) == false
    end

    test "identifies enterprise OIDs" do
      assert OID.enterprise?([1, 3, 6, 1, 4, 1, 9, 1, 1]) == true
      assert OID.enterprise?([1, 3, 6, 1, 2, 1, 1, 1, 0]) == false
    end

    test "identifies experimental OIDs" do
      assert OID.experimental?([1, 3, 6, 1, 3, 1]) == true
      assert OID.experimental?([1, 3, 6, 1, 2, 1]) == false
    end

    test "identifies private OIDs" do
      assert OID.private?([1, 3, 6, 1, 4, 2]) == true
      assert OID.private?([1, 3, 6, 1, 2, 1]) == false
    end

    test "extracts enterprise numbers" do
      {:ok, enterprise_num} = OID.get_enterprise_number([1, 3, 6, 1, 4, 1, 9, 1, 1])
      assert enterprise_num == 9

      {:ok, enterprise_num2} = OID.get_enterprise_number([1, 3, 6, 1, 4, 1, 12_345])
      assert enterprise_num2 == 12_345
    end

    test "rejects non-enterprise OIDs for enterprise number extraction" do
      assert {:error, :not_enterprise_oid} = OID.get_enterprise_number([1, 3, 6, 1, 2, 1])
      # Too short
      assert {:error, :not_enterprise_oid} = OID.get_enterprise_number([1, 3, 6, 1, 4, 1])
    end
  end

  describe "Error handling and edge cases" do
    test "handles invalid inputs gracefully" do
      assert {:error, :invalid_input} = OID.string_to_list(:not_binary)
      assert {:error, :invalid_input} = OID.list_to_string(:not_list)
      assert OID.child_of?(:not_list, [1, 2, 3]) == false
      assert OID.child_of?([1, 2, 3], :not_list) == false
      assert {:error, :invalid_input} = OID.get_parent(:not_list)
    end

    test "handles whitespace in OID strings" do
      {:ok, oid} = OID.string_to_list("  1.3.6.1.2.1  ")
      assert oid == [1, 3, 6, 1, 2, 1]

      {:ok, oid_with_dot} = OID.string_to_list("  .1.3.6.1.2.1  ")
      assert oid_with_dot == [1, 3, 6, 1, 2, 1]
    end

    test "handles very long OIDs" do
      long_oid = [1, 3, 6, 1] ++ Enum.to_list(1..100)
      {:ok, oid_string} = OID.list_to_string(long_oid)
      {:ok, parsed_oid} = OID.string_to_list(oid_string)
      assert parsed_oid == long_oid
    end

    test "handles zero components in OID" do
      oid_with_zeros = [1, 3, 0, 1, 0, 0, 1]
      {:ok, oid_string} = OID.list_to_string(oid_with_zeros)
      {:ok, parsed_oid} = OID.string_to_list(oid_string)
      assert parsed_oid == oid_with_zeros
    end
  end

  describe "Performance and stress testing" do
    test "handles many OID operations efficiently" do
      # Generate 1000 OIDs
      oids =
        for i <- 1..1000 do
          [1, 3, 6, 1, 4, 1, 9999, i]
        end

      # Test sorting performance
      start_time = System.monotonic_time(:microsecond)
      sorted_oids = OID.sort(oids)
      end_time = System.monotonic_time(:microsecond)

      # Should complete in reasonable time (< 10ms)
      assert end_time - start_time < 10_000
      assert length(sorted_oids) == 1000
    end

    test "handles concurrent OID operations" do
      # Test thread safety
      tasks =
        for i <- 1..50 do
          Task.async(fn ->
            oid = [1, 3, 6, 1, 4, 1, 9999, i]
            {:ok, oid_string} = OID.list_to_string(oid)
            {:ok, parsed_oid} = OID.string_to_list(oid_string)
            {i, oid, parsed_oid}
          end)
        end

      results = Task.await_many(tasks, 1000)

      # Verify all operations completed successfully
      assert length(results) == 50

      for {i, original_oid, parsed_oid} <- results do
        assert original_oid == parsed_oid
        assert List.last(original_oid) == i
      end
    end
  end
end
