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

  alias SnmpKit.SnmpLib.MIB.Utilities

  describe "register_oid/4" do
    test "registers a new OID" do
      oid_table = %{}

      result = Utilities.register_oid("sysDescr", [1, 3, 6, 1, 2, 1, 1, 1], :resolved, oid_table)

      assert Map.has_key?(result, "sysDescr")
      entry = result["sysDescr"]
      assert entry.name == "sysDescr"
      assert entry.oid == [1, 3, 6, 1, 2, 1, 1, 1]
      assert entry.status == :resolved
      assert entry.parent == [1, 3, 6, 1, 2, 1, 1]
    end

    test "registers root OID without parent" do
      oid_table = %{}

      result = Utilities.register_oid("root", [1], :resolved, oid_table)

      entry = result["root"]
      assert entry.parent == nil
    end
  end

  describe "resolve_oids/1" do
    test "resolves OID tree successfully" do
      oid_table = %{
        "root" => %{
          name: "root",
          oid: [1],
          status: :resolved,
          parent: nil,
          children: []
        },
        "child" => %{
          name: "child",
          oid: [%{name: "root", value: 3}],
          status: :unresolved,
          parent: nil,
          children: []
        }
      }

      # This tests OID resolution - should either succeed or return error
      result = Utilities.resolve_oids(oid_table)

      # Should return either ok or error
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "handles already resolved OIDs" do
      oid_table = %{
        "sysDescr" => %{
          name: "sysDescr",
          oid: [1, 3, 6, 1, 2, 1, 1, 1],
          status: :resolved,
          parent: [1, 3, 6, 1, 2, 1, 1],
          children: []
        }
      }

      assert {:ok, result} = Utilities.resolve_oids(oid_table)
      assert result == oid_table
    end
  end

  describe "tr_oid/2" do
    test "translates resolved OID name to numeric" do
      oid_table = %{
        "sysDescr" => %{
          name: "sysDescr",
          oid: [1, 3, 6, 1, 2, 1, 1, 1],
          status: :resolved,
          parent: nil,
          children: []
        }
      }

      assert {:ok, [1, 3, 6, 1, 2, 1, 1, 1]} = Utilities.tr_oid("sysDescr", oid_table)
    end

    test "returns error for unresolved OID" do
      oid_table = %{
        "test" => %{
          name: "test",
          oid: [1, 2, 3],
          status: :unresolved,
          parent: nil,
          children: []
        }
      }

      assert {:error, :unresolved} = Utilities.tr_oid("test", oid_table)
    end

    test "returns error for unknown OID" do
      oid_table = %{}

      assert {:error, :not_found} = Utilities.tr_oid("unknown", oid_table)
    end
  end

  describe "update_me_oids/3" do
    test "updates MIB entries with resolved OIDs" do
      oid_table = %{
        "system" => %{
          name: "system",
          oid: [1, 3, 6, 1, 2, 1, 1],
          status: :resolved,
          parent: nil,
          children: []
        }
      }

      entries = [
        %{name: "sysDescr", oid: "system"},
        %{name: "other", value: 123}
      ]

      result = Utilities.update_me_oids(entries, oid_table, :silent)

      # First entry should have resolved OID
      first = Enum.at(result, 0)
      assert first.oid == [1, 3, 6, 1, 2, 1, 1]

      # Second entry has no oid field, should remain unchanged
      second = Enum.at(result, 1)
      assert second.value == 123
    end

    test "handles entries without OIDs" do
      entries = [%{name: "test", value: 123}]
      result = Utilities.update_me_oids(entries, %{}, :debug)

      assert result == entries
    end
  end

  describe "make_asn1_type/1" do
    test "validates integer type" do
      assert {:ok, type_def} = Utilities.make_asn1_type({:integer, []})
      assert type_def.type == :integer
    end

    test "validates integer with range constraint" do
      assert {:ok, type_def} = Utilities.make_asn1_type({:integer, [{:range, 0, 100}]})
      assert type_def.type == :integer
      assert type_def.constraints == [{:range, 0, 100}]
    end

    test "validates octet string type" do
      assert {:ok, type_def} = Utilities.make_asn1_type({:octet_string, []})
      assert type_def.type == :octet_string
    end

    test "validates octet string with size constraint" do
      assert {:ok, type_def} = Utilities.make_asn1_type({:octet_string, [1, {:range, 0, 255}]})
      assert type_def.type == :octet_string
    end

    test "validates object identifier type" do
      assert {:ok, type_def} = Utilities.make_asn1_type({:object_identifier})
      assert type_def.type == :object_identifier
    end

    test "validates named type" do
      assert {:ok, type_def} = Utilities.make_asn1_type({:named_type, "DisplayString"})
      assert type_def.type == :named_type
      assert type_def.name == "DisplayString"
    end

    test "validates atom type" do
      assert {:ok, type_def} = Utilities.make_asn1_type(:integer)
      assert type_def.type == :integer
    end

    test "rejects invalid type" do
      assert {:error, msg} = Utilities.make_asn1_type({:invalid_type, "bad"})
      assert msg =~ "Invalid type definition"
    end

    test "rejects invalid integer constraint" do
      assert {:error, msg} = Utilities.make_asn1_type({:integer, [{:range, 100, 0}]})
      assert msg =~ "Invalid integer constraint"
    end

    test "rejects invalid size constraint" do
      assert {:error, msg} = Utilities.make_asn1_type({:octet_string, [-1]})
      assert msg =~ "Invalid size constraint"
    end
  end

  describe "test_kibbles/2" do
    test "validates unique bit names and values" do
      bits = [
        %{name: "bit1", value: 0},
        %{name: "bit2", value: 1},
        %{name: "bit3", value: 2}
      ]

      assert :ok = Utilities.test_kibbles(bits, :debug)
    end

    test "rejects duplicate bit names" do
      bits = [
        %{name: "bit1", value: 0},
        %{name: "bit1", value: 1}
      ]

      assert {:error, msg} = Utilities.test_kibbles(bits, :warning)
      assert msg =~ "Duplicate bit names"
    end

    test "rejects duplicate bit values" do
      bits = [
        %{name: "bit1", value: 0},
        %{name: "bit2", value: 0}
      ]

      assert {:error, msg} = Utilities.test_kibbles(bits, :warning)
      assert msg =~ "Duplicate bit values"
    end
  end

  describe "allow_size_rfc1902/1" do
    test "allows size for octet_string" do
      assert Utilities.allow_size_rfc1902(:octet_string)
    end

    test "disallows size for integer" do
      refute Utilities.allow_size_rfc1902(:integer)
    end

    test "disallows size for object_identifier" do
      refute Utilities.allow_size_rfc1902(:object_identifier)
    end
  end

  describe "check_sub_ids/3" do
    test "accepts valid sub-identifiers" do
      assert :ok = Utilities.check_sub_ids([1, 3, 6, 1, 2, 1], 0, 255)
    end

    test "rejects sub-identifier below minimum" do
      assert {:error, msg} = Utilities.check_sub_ids([1, 3, -1], 0, 255)
      assert msg =~ "out of range"
    end

    test "rejects sub-identifier above maximum" do
      assert {:error, msg} = Utilities.check_sub_ids([1, 3, 300], 0, 255)
      assert msg =~ "out of range"
    end
  end

  describe "print_error/2" do
    test "prints error when not silent" do
      assert :ok = Utilities.print_error("Test error", :warning)
    end

    test "suppresses error when silent" do
      assert :ok = Utilities.print_error("Test error", :silent)
    end
  end

  describe "print_error/3" do
    test "prints formatted error when not silent" do
      assert :ok = Utilities.print_error("Error: ~p", ["test"], :info)
    end

    test "suppresses formatted error when silent" do
      assert :ok = Utilities.print_error("Error: ~p", ["test"], :silent)
    end
  end

  describe "compilation_error/1" do
    test "throws error with message" do
      assert catch_throw(Utilities.compilation_error("Test error")) == {:error, "Test error"}
    end
  end

  describe "compilation_error/2" do
    test "throws formatted error" do
      result = catch_throw(Utilities.compilation_error("Error: ~s", ["test"]))
      assert {:error, msg} = result
      assert msg =~ "Error: test"
    end
  end

  describe "vprint/6" do
    test "prints debug message when verbosity is high enough" do
      assert :ok =
               Utilities.vprint(:debug, :debug, "TestModule", "test_function", "Message: ~s", [
                 "test"
               ])
    end

    test "suppresses message when verbosity is too low" do
      assert :ok =
               Utilities.vprint(:silent, :debug, "TestModule", "test_function", "Message", [])
    end
  end

  describe "printable?/2" do
    test "returns true when current >= required" do
      assert Utilities.printable?(:debug, :warning)
      assert Utilities.printable?(:info, :warning)
      assert Utilities.printable?(:warning, :warning)
    end

    test "returns false when current < required" do
      refute Utilities.printable?(:warning, :debug)
      refute Utilities.printable?(:silent, :info)
    end
  end

  describe "vvalidate/1" do
    test "validates valid verbosity levels" do
      assert {:ok, :silent} = Utilities.vvalidate(:silent)
      assert {:ok, :warning} = Utilities.vvalidate(:warning)
      assert {:ok, :info} = Utilities.vvalidate(:info)
      assert {:ok, :debug} = Utilities.vvalidate(:debug)
    end

    test "rejects invalid verbosity" do
      assert {:error, msg} = Utilities.vvalidate(:invalid)
      assert msg =~ "Invalid verbosity level"
    end

    test "rejects non-atom verbosity" do
      assert {:error, msg} = Utilities.vvalidate("debug")
      assert msg =~ "Invalid verbosity level"
    end
  end

  describe "key1search/2" do
    test "finds tuple by first element" do
      list = [{:a, 1}, {:b, 2}, {:c, 3}]

      assert {:value, {:b, 2}} = Utilities.key1search(:b, list)
    end

    test "returns false when not found" do
      list = [{:a, 1}, {:b, 2}]

      assert false == Utilities.key1search(:c, list)
    end
  end

  describe "key1search/3" do
    test "returns tuple when found" do
      list = [{:a, 1}, {:b, 2}]

      assert {:b, 2} = Utilities.key1search(:b, list, :default)
    end

    test "returns default when not found" do
      list = [{:a, 1}]

      assert :default = Utilities.key1search(:b, list, :default)
    end
  end

  describe "set_dir/2" do
    test "joins directory when filename has no path" do
      result = Utilities.set_dir("test.mib", "/usr/share/mibs")

      assert result == "/usr/share/mibs/test.mib"
    end

    test "preserves absolute path" do
      result = Utilities.set_dir("/home/user/test.mib", "/usr/share/mibs")

      assert result == "/home/user/test.mib"
    end

    test "preserves relative path with directory" do
      result = Utilities.set_dir("mibs/test.mib", "/usr/share/mibs")

      assert result == "mibs/test.mib"
    end
  end

  describe "lookup/2" do
    test "finds item by key in tuple list" do
      list = [{:a, 1}, {:b, 2}]

      assert {:ok, {:b, 2}} = Utilities.lookup(:b, list)
    end

    test "finds item by name in map list" do
      list = [%{name: "test1", value: 1}, %{name: "test2", value: 2}]

      assert {:ok, %{name: "test2", value: 2}} = Utilities.lookup("test2", list)
    end

    test "finds item by id in map list" do
      list = [%{id: 1, data: "a"}, %{id: 2, data: "b"}]

      assert {:ok, %{id: 2, data: "b"}} = Utilities.lookup(2, list)
    end

    test "returns error when not found" do
      list = [{:a, 1}]

      assert {:error, :not_found} = Utilities.lookup(:b, list)
    end
  end
end
