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

  alias Towerops.Snmp.MibParser

  describe "parse_mib_content/1" do
    test "parses simple OBJECT-TYPE definitions" do
      content = """
      sysDescr OBJECT-TYPE
        ::= { system 1 }

      sysName OBJECT-TYPE
        ::= { system 5 }
      """

      oids = MibParser.parse_mib_content(content)

      assert is_map(oids)
      # These won't resolve without parent "system" being defined as a root
      # but they should be extracted as assignments
    end

    test "parses OBJECT IDENTIFIER definitions" do
      content = """
      enterprises OBJECT IDENTIFIER ::= { private 1 }
      mikrotik OBJECT IDENTIFIER ::= { enterprises 14988 }
      """

      oids = MibParser.parse_mib_content(content)

      assert is_map(oids)
      # enterprises should resolve to 1.3.6.1.4.1
      assert Map.get(oids, "enterprises") == "1.3.6.1.4.1"
    end

    test "removes comments before parsing" do
      content = """
      -- This is a comment
      sysDescr OBJECT-TYPE  -- inline comment
        ::= { system 1 }
      -- Another comment
      """

      oids = MibParser.parse_mib_content(content)

      # Should parse without errors
      assert is_map(oids)
    end

    test "resolves OIDs with known roots" do
      content = """
      parent OBJECT IDENTIFIER ::= { enterprises 999 }
      myObject OBJECT-TYPE ::= { parent 1 }
      """

      oids = MibParser.parse_mib_content(content)

      assert Map.get(oids, "parent") == "1.3.6.1.4.1.999"
      assert Map.get(oids, "myObject") == "1.3.6.1.4.1.999.1"
    end

    test "resolves nested OID assignments" do
      content = """
      parent OBJECT IDENTIFIER ::= { enterprises 100 }
      child OBJECT-TYPE ::= { parent 5 }
      grandchild OBJECT-TYPE ::= { child 3 }
      """

      oids = MibParser.parse_mib_content(content)

      assert Map.get(oids, "parent") == "1.3.6.1.4.1.100"
      assert Map.get(oids, "child") == "1.3.6.1.4.1.100.5"
      assert Map.get(oids, "grandchild") == "1.3.6.1.4.1.100.5.3"
    end

    test "includes well-known root OIDs" do
      oids = MibParser.parse_mib_content("")

      assert Map.get(oids, "iso") == "1"
      assert Map.get(oids, "internet") == "1.3.6.1"
      assert Map.get(oids, "mib-2") == "1.3.6.1.2.1"
      assert Map.get(oids, "enterprises") == "1.3.6.1.4.1"
    end

    test "handles empty content" do
      oids = MibParser.parse_mib_content("")

      assert is_map(oids)
      # Should have root OIDs
      assert map_size(oids) > 0
    end

    test "handles multiline definitions" do
      content = """
      sysDescr OBJECT-TYPE
        SYNTAX      DisplayString (SIZE (0..255))
        MAX-ACCESS  read-only
        STATUS      current
        DESCRIPTION
          "A textual description of the entity."
        ::= { system 1 }
      """

      oids = MibParser.parse_mib_content(content)

      assert is_map(oids)
    end

    test "extracts multiple objects from real-world MIB structure" do
      content = """
      mycompany OBJECT IDENTIFIER ::= { enterprises 12345 }
      system OBJECT IDENTIFIER ::= { mycompany 1 }

      sysDescr OBJECT-TYPE
        ::= { system 1 }

      sysObjectID OBJECT-TYPE
        ::= { system 2 }

      sysUpTime OBJECT-TYPE
        ::= { system 3 }
      """

      oids = MibParser.parse_mib_content(content)

      assert Map.get(oids, "mycompany") == "1.3.6.1.4.1.12345"
      assert Map.get(oids, "system") == "1.3.6.1.4.1.12345.1"
      assert Map.get(oids, "sysDescr") == "1.3.6.1.4.1.12345.1.1"
      assert Map.get(oids, "sysObjectID") == "1.3.6.1.4.1.12345.1.2"
      assert Map.get(oids, "sysUpTime") == "1.3.6.1.4.1.12345.1.3"
    end
  end

  describe "parse_mib_file/1" do
    test "returns error for non-existent file" do
      assert {:error, :enoent} = MibParser.parse_mib_file("/nonexistent/file")
    end

    test "returns error with descriptive message for missing file" do
      result = MibParser.parse_mib_file("/tmp/does_not_exist_#{:rand.uniform(1000)}")

      assert match?({:error, _}, result)
    end

    @tag :tmp_dir
    test "parses valid MIB file", %{tmp_dir: tmp_dir} do
      mib_file = Path.join(tmp_dir, "TEST-MIB")

      File.write!(mib_file, """
      TEST-MIB DEFINITIONS ::= BEGIN

      testObject OBJECT-TYPE ::= { enterprises 9999 }
      testChild OBJECT-TYPE ::= { testObject 1 }

      END
      """)

      assert {:ok, oids} = MibParser.parse_mib_file(mib_file)
      assert is_map(oids)
      assert Map.get(oids, "testObject") == "1.3.6.1.4.1.9999"
      assert Map.get(oids, "testChild") == "1.3.6.1.4.1.9999.1"
    end

    @tag :tmp_dir
    test "handles MIB file with complex structure", %{tmp_dir: tmp_dir} do
      mib_file = Path.join(tmp_dir, "COMPLEX-MIB")

      File.write!(mib_file, """
      COMPLEX-MIB DEFINITIONS ::= BEGIN

      -- Root object
      myCompany OBJECT IDENTIFIER ::= { enterprises 12345 }

      -- Product line
      products OBJECT IDENTIFIER ::= { myCompany 1 }
      services OBJECT IDENTIFIER ::= { myCompany 2 }

      -- Specific products
      productA OBJECT IDENTIFIER ::= { products 1 }
      productB OBJECT IDENTIFIER ::= { products 2 }

      -- Product A metrics
      productAUptime OBJECT-TYPE ::= { productA 1 }
      productAStatus OBJECT-TYPE ::= { productA 2 }

      END
      """)

      assert {:ok, oids} = MibParser.parse_mib_file(mib_file)

      assert Map.get(oids, "myCompany") == "1.3.6.1.4.1.12345"
      assert Map.get(oids, "products") == "1.3.6.1.4.1.12345.1"
      assert Map.get(oids, "productA") == "1.3.6.1.4.1.12345.1.1"
      assert Map.get(oids, "productAUptime") == "1.3.6.1.4.1.12345.1.1.1"
    end
  end

  describe "validate_oid/3" do
    @tag :tmp_dir
    test "returns :ok when OID matches", %{tmp_dir: tmp_dir} do
      mib_file = Path.join(tmp_dir, "VALID-MIB")

      File.write!(mib_file, """
      testObj OBJECT-TYPE ::= { enterprises 1234 }
      """)

      assert :ok = MibParser.validate_oid(mib_file, "testObj", "1.3.6.1.4.1.1234")
    end

    @tag :tmp_dir
    test "returns error when OID does not match", %{tmp_dir: tmp_dir} do
      mib_file = Path.join(tmp_dir, "MISMATCH-MIB")

      File.write!(mib_file, """
      testObj OBJECT-TYPE ::= { enterprises 1234 }
      """)

      assert {:error, msg} = MibParser.validate_oid(mib_file, "testObj", "1.3.6.1.4.1.9999")
      assert msg =~ "OID mismatch"
      assert msg =~ "expected 1.3.6.1.4.1.9999"
      assert msg =~ "got 1.3.6.1.4.1.1234"
    end

    @tag :tmp_dir
    test "returns error when object not found in MIB", %{tmp_dir: tmp_dir} do
      mib_file = Path.join(tmp_dir, "MISSING-MIB")

      File.write!(mib_file, """
      existingObj OBJECT-TYPE ::= { enterprises 1234 }
      """)

      assert {:error, msg} = MibParser.validate_oid(mib_file, "missingObj", "1.3.6.1.4.1.9999")
      assert msg =~ "not found in MIB"
    end

    test "returns error when MIB file does not exist" do
      assert {:error, msg} = MibParser.validate_oid("/nonexistent", "obj", "1.2.3")
      assert msg =~ "Failed to parse MIB"
    end
  end

  describe "list_mib_files/0" do
    test "returns a list" do
      files = MibParser.list_mib_files()
      assert is_list(files)
    end

    test "returns absolute paths" do
      files = MibParser.list_mib_files()

      if files != [] do
        Enum.each(files, fn path ->
          assert String.starts_with?(path, "/")
        end)
      end
    end

    test "only returns files ending with MIB" do
      files = MibParser.list_mib_files()

      Enum.each(files, fn path ->
        assert String.ends_with?(Path.basename(path), "MIB")
      end)
    end

    test "searches in subdirectories" do
      files = MibParser.list_mib_files()

      # Should find MIBs in different subdirectories
      if files != [] do
        # At least some files should be in subdirectories
        subdirs =
          files
          |> Enum.map(&Path.dirname/1)
          |> Enum.uniq()

        # Should have multiple subdirectories
        assert subdirs != [], "Expected at least 1 subdirectory"
      end
    end
  end

  describe "edge cases" do
    test "handles whitespace variations in assignments" do
      content = """
      obj1 OBJECT-TYPE::={parent 1}
      obj2 OBJECT-TYPE ::= {parent 2}
      obj3 OBJECT-TYPE ::=  {  parent  3  }
      """

      oids = MibParser.parse_mib_content(content)
      assert is_map(oids)
    end

    test "handles definitions with hyphens in names" do
      content = """
      my-object OBJECT-TYPE ::= { mib-2 999 }
      """

      # Hyphens in identifiers are actually invalid in the regex
      # This documents current behavior
      oids = MibParser.parse_mib_content(content)
      # Won't match due to \\w+ in regex
      refute Map.has_key?(oids, "my-object")
    end

    test "handles numeric parent references" do
      content = """
      obj OBJECT-TYPE ::= { 1 3 6 1 2 1 999 }
      """

      # This format isn't supported by the parser
      # Documents current limitation
      oids = MibParser.parse_mib_content(content)
      refute Map.has_key?(oids, "obj")
    end

    test "preserves root OIDs when parsing empty MIB" do
      oids = MibParser.parse_mib_content("")

      # All well-known roots should be present
      expected_roots = [
        "iso",
        "org",
        "dod",
        "internet",
        "directory",
        "mgmt",
        "mib-2",
        "experimental",
        "private",
        "enterprises",
        "security",
        "snmpV2",
        "snmpModules"
      ]

      Enum.each(expected_roots, fn root ->
        assert Map.has_key?(oids, root), "Missing root OID: #{root}"
      end)
    end
  end
end
