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

  alias SnmpKit.SnmpLib.MIB.Parser

  describe "init_parser/0" do
    @tag :yecc_required
    test "returns ok tuple with module name" do
      result = Parser.init_parser()
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "returns same module name on repeated calls" do
      {:ok, module1} = Parser.init_parser()
      {:ok, module2} = Parser.init_parser()
      assert module1 == module2
    end
  end

  describe "tokenize/1" do
    test "tokenizes empty string" do
      result = Parser.tokenize("")
      assert {:ok, tokens} = result
      assert is_list(tokens)
    end

    test "tokenizes simple MIB header" do
      mib = "TEST-MIB DEFINITIONS ::= BEGIN END"
      result = Parser.tokenize(mib)
      assert {:ok, tokens} = result
      assert is_list(tokens)
      refute Enum.empty?(tokens)
    end

    test "tokenizes reserved words" do
      mib = "DEFINITIONS BEGIN END IMPORTS FROM OBJECT-TYPE"
      result = Parser.tokenize(mib)
      assert {:ok, tokens} = result
      assert length(tokens) >= 6
    end

    test "tokenizes identifiers" do
      mib = "testObject myIdentifier"
      result = Parser.tokenize(mib)
      assert {:ok, tokens} = result
      assert length(tokens) >= 2
    end

    test "tokenizes numbers" do
      mib = "123 456 789"
      result = Parser.tokenize(mib)
      assert {:ok, tokens} = result
      assert length(tokens) >= 3
    end

    test "tokenizes special characters" do
      mib = "::= { } ( ) , ;"
      result = Parser.tokenize(mib)
      assert {:ok, tokens} = result
      assert length(tokens) > 5
    end

    test "tokenizes complete OBJECT-TYPE" do
      mib = """
      testObject OBJECT-TYPE
        SYNTAX Integer32
        MAX-ACCESS read-only
        STATUS current
        DESCRIPTION "Test"
        ::= { test 1 }
      """

      result = Parser.tokenize(mib)
      assert {:ok, tokens} = result
      assert length(tokens) > 10
    end

    test "handles multiline input" do
      mib = """
      TEST-MIB
      DEFINITIONS
      ::=
      BEGIN
      END
      """

      result = Parser.tokenize(mib)
      assert {:ok, tokens} = result
      assert length(tokens) > 4
    end

    test "handles comments" do
      mib = "test -- this is a comment\nOBJECT-TYPE"
      result = Parser.tokenize(mib)
      assert {:ok, tokens} = result
      assert length(tokens) >= 2
    end

    test "handles hex atoms (8+ characters)" do
      mib = "12345678 abcdef01"
      result = Parser.tokenize(mib)
      assert {:ok, tokens} = result
      assert is_list(tokens)
    end

    test "handles short hex-like identifiers" do
      # Short hex patterns like d1, d2 should remain as atoms
      mib = "d1 d2 a1"
      result = Parser.tokenize(mib)
      assert {:ok, tokens} = result
      assert is_list(tokens)
    end
  end

  describe "parse/1" do
    @tag :yecc_required
    test "returns error for empty string" do
      result = Parser.parse("")
      assert {:error, _reason} = result
    end

    @tag :yecc_required
    test "returns error for invalid MIB content" do
      result = Parser.parse("invalid mib content")
      assert {:error, _reason} = result
    end

    @tag :yecc_required
    test "accepts minimal MIB header" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "parses MIB with IMPORTS" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      IMPORTS
        MODULE-IDENTITY FROM SNMPv2-SMI;
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "parses MIB with OBJECT-TYPE" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testObject OBJECT-TYPE
        SYNTAX Integer32
        MAX-ACCESS read-only
        STATUS current
        DESCRIPTION "Test object"
        ::= { test 1 }
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "returns structured error on parse failure" do
      result = Parser.parse("BEGIN END")
      assert {:error, _reason} = result
    end
  end

  describe "parse_tokens/1" do
    @tag :yecc_required
    test "accepts token list" do
      # Minimal token list representing "TEST-MIB DEFINITIONS ::= BEGIN END"
      tokens = [
        {:variable, 1, :TEST_MIB},
        {:DEFINITIONS, 1},
        {:"::=", 1},
        {:BEGIN, 1},
        {:END, 1},
        {:"$end", 1}
      ]

      result = Parser.parse_tokens(tokens)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "returns error for invalid token sequence" do
      tokens = [{:BEGIN, 1}, {:END, 1}]
      result = Parser.parse_tokens(tokens)
      assert {:error, _reason} = result
    end

    @tag :yecc_required
    test "accepts empty token list" do
      tokens = []
      result = Parser.parse_tokens(tokens)
      assert {:error, _reason} = result
    end
  end

  describe "mibdirs/1" do
    @tag :yecc_required
    test "accepts empty directory list" do
      result = Parser.mibdirs([])
      assert is_map(result)
      assert map_size(result) == 0
    end

    @tag :yecc_required
    test "returns error for non-existent directory" do
      result = Parser.mibdirs(["/nonexistent/path/to/mibs"])
      assert is_map(result)
      assert Map.has_key?(result, "/nonexistent/path/to/mibs")
    end

    @tag :yecc_required
    test "processes multiple directories" do
      dirs = ["/tmp/mibs1", "/tmp/mibs2"]
      result = Parser.mibdirs(dirs)
      assert is_map(result)
      assert map_size(result) == 2
    end

    @tag :yecc_required
    test "result contains directory structure" do
      result = Parser.mibdirs(["/nonexistent"])
      dir_result = result["/nonexistent"]
      assert is_map(dir_result)
      assert Map.has_key?(dir_result, :directory)
      assert Map.has_key?(dir_result, :total)
      assert Map.has_key?(dir_result, :success)
      assert Map.has_key?(dir_result, :failures)
    end
  end

  describe "tokenize integration" do
    test "converts long hex atoms to integers" do
      # Tokenize content with long hex pattern (8+ characters)
      mib = "12345678"
      {:ok, tokens} = Parser.tokenize(mib)

      # Should convert long hex atom to integer token
      assert Enum.any?(tokens, fn
               {:integer, _, _} -> true
               _ -> false
             end)
    end

    test "preserves short hex-like identifiers as atoms" do
      # Short patterns should remain as atoms
      mib = "d1"
      {:ok, tokens} = Parser.tokenize(mib)

      # Should NOT convert short patterns to integers
      assert Enum.any?(tokens, fn
               {:atom, _, _} -> true
               _ -> false
             end)
    end

    test "handles mixed hex and regular identifiers" do
      mib = "test 12345678 identifier"
      {:ok, tokens} = Parser.tokenize(mib)
      assert is_list(tokens)
      assert length(tokens) >= 3
    end
  end

  describe "error handling" do
    @tag :yecc_required
    test "parse handles malformed syntax gracefully" do
      malformed = "OBJECT-TYPE ::= {{{ test"
      result = Parser.parse(malformed)
      assert {:error, _reason} = result
    end

    @tag :yecc_required
    test "parse handles incomplete MIB" do
      incomplete = "TEST-MIB DEFINITIONS ::= BEGIN"
      result = Parser.parse(incomplete)
      assert {:error, _reason} = result
    end

    @tag :yecc_required
    test "parse handles missing keywords" do
      missing_keyword = "TEST-MIB ::= BEGIN END"
      result = Parser.parse(missing_keyword)
      assert {:error, _reason} = result
    end

    test "tokenize handles special characters" do
      special = "@#$%^&*"
      result = Parser.tokenize(special)
      # Should either succeed with tokens or return error
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end
  end

  describe "MIB structure parsing" do
    @tag :yecc_required
    test "parses MODULE-IDENTITY" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testMIB MODULE-IDENTITY
        LAST-UPDATED "202501010000Z"
        ORGANIZATION "Test Org"
        CONTACT-INFO "test@example.com"
        DESCRIPTION "Test MIB"
        ::= { test 1 }
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "parses TEXTUAL-CONVENTION" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      TestTC ::= TEXTUAL-CONVENTION
        STATUS current
        DESCRIPTION "Test textual convention"
        SYNTAX Integer32
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "parses OBJECT-GROUP" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testGroup OBJECT-GROUP
        OBJECTS { testObject1, testObject2 }
        STATUS current
        DESCRIPTION "Test object group"
        ::= { test 1 }
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "parses EXPORTS clause" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      EXPORTS testObject, testGroup;
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end
  end

  describe "complex MIB parsing" do
    @tag :yecc_required
    test "parses MIB with multiple IMPORTS" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      IMPORTS
        MODULE-IDENTITY, OBJECT-TYPE, Integer32
          FROM SNMPv2-SMI
        TEXTUAL-CONVENTION FROM SNMPv2-TC;
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "parses MIB with SEQUENCE OF" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      TestTable ::= SEQUENCE OF TestEntry
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "parses OBJECT-TYPE with INDEX" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testEntry OBJECT-TYPE
        SYNTAX TestEntry
        MAX-ACCESS not-accessible
        STATUS current
        DESCRIPTION "Test entry"
        INDEX { testIndex }
        ::= { testTable 1 }
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "parses OBJECT-TYPE with SIZE constraint" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testObject OBJECT-TYPE
        SYNTAX OCTET STRING (SIZE (0..255))
        MAX-ACCESS read-only
        STATUS current
        DESCRIPTION "Test with size"
        ::= { test 1 }
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "parses OBJECT-TYPE with range constraint" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testObject OBJECT-TYPE
        SYNTAX Integer32 (0..100)
        MAX-ACCESS read-write
        STATUS current
        DESCRIPTION "Test with range"
        ::= { test 1 }
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end
  end

  describe "description cleaning" do
    @tag :yecc_required
    test "parses OBJECT-TYPE with multiline description" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testObject OBJECT-TYPE
        SYNTAX Integer32
        MAX-ACCESS read-only
        STATUS current
        DESCRIPTION "This is a
                     multiline
                     description"
        ::= { test 1 }
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "parses OBJECT-TYPE with very long description" do
      long_desc = String.duplicate("This is a very long description. ", 100)

      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testObject OBJECT-TYPE
        SYNTAX Integer32
        MAX-ACCESS read-only
        STATUS current
        DESCRIPTION "#{long_desc}"
        ::= { test 1 }
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "parses OBJECT-TYPE with empty description" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testObject OBJECT-TYPE
        SYNTAX Integer32
        MAX-ACCESS read-only
        STATUS current
        DESCRIPTION ""
        ::= { test 1 }
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end
  end

  describe "edge cases" do
    @tag :yecc_required
    test "handles very large MIB file" do
      # Generate large MIB with many OBJECT-TYPE definitions
      objects =
        Enum.map(1..100, fn i ->
          """
          testObject#{i} OBJECT-TYPE
            SYNTAX Integer32
            MAX-ACCESS read-only
            STATUS current
            DESCRIPTION "Test object #{i}"
            ::= { test #{i} }
          """
        end)

      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      #{Enum.join(objects, "\n")}
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    test "tokenize handles very long identifier" do
      long_id = String.duplicate("a", 200)
      result = Parser.tokenize(long_id)
      assert {:ok, tokens} = result
      assert is_list(tokens)
    end

    test "tokenize handles many tokens" do
      identifiers = Enum.map_join(1..500, " ", fn i -> "id#{i}" end)
      result = Parser.tokenize(identifiers)
      assert {:ok, tokens} = result
      assert is_list(tokens)
    end

    @tag :yecc_required
    test "parses MIB with deeply nested OIDs" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testObject OBJECT-TYPE
        SYNTAX Integer32
        MAX-ACCESS read-only
        STATUS current
        DESCRIPTION "Test"
        ::= { iso org dod internet private enterprises test 1 2 3 4 5 }
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end
  end

  describe "real-world MIB patterns" do
    @tag :yecc_required
    test "parses SNMPv2-style MIB" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      IMPORTS
        MODULE-IDENTITY, OBJECT-TYPE, Integer32
          FROM SNMPv2-SMI
        MODULE-COMPLIANCE, OBJECT-GROUP
          FROM SNMPv2-CONF;

      testMIB MODULE-IDENTITY
        LAST-UPDATED "202501010000Z"
        ORGANIZATION "Test Organization"
        CONTACT-INFO "admin@test.com"
        DESCRIPTION "Test MIB for unit testing"
        REVISION "202501010000Z"
        DESCRIPTION "Initial version"
        ::= { test 1 }
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "parses MIB with table definition" do
      mib = """
      TEST-MIB DEFINITIONS ::= BEGIN
      testTable OBJECT-TYPE
        SYNTAX SEQUENCE OF TestEntry
        MAX-ACCESS not-accessible
        STATUS current
        DESCRIPTION "Test table"
        ::= { test 1 }

      testEntry OBJECT-TYPE
        SYNTAX TestEntry
        MAX-ACCESS not-accessible
        STATUS current
        DESCRIPTION "Test entry"
        INDEX { testIndex }
        ::= { testTable 1 }
      END
      """

      result = Parser.parse(mib)
      assert is_tuple(result) and elem(result, 0) in [:ok, :error]
    end

    @tag :yecc_required
    test "parses MIB with various access levels" do
      access_levels = ["read-only", "read-write", "read-create", "not-accessible", "accessible-for-notify"]

      for access <- access_levels do
        mib = """
        TEST-MIB DEFINITIONS ::= BEGIN
        testObject OBJECT-TYPE
          SYNTAX Integer32
          MAX-ACCESS #{access}
          STATUS current
          DESCRIPTION "Test"
          ::= { test 1 }
        END
        """

        result = Parser.parse(mib)
        assert is_tuple(result) and elem(result, 0) in [:ok, :error]
      end
    end

    @tag :yecc_required
    test "parses MIB with various status values" do
      statuses = ["current", "deprecated", "obsolete"]

      for status <- statuses do
        mib = """
        TEST-MIB DEFINITIONS ::= BEGIN
        testObject OBJECT-TYPE
          SYNTAX Integer32
          MAX-ACCESS read-only
          STATUS #{status}
          DESCRIPTION "Test"
          ::= { test 1 }
        END
        """

        result = Parser.parse(mib)
        assert is_tuple(result) and elem(result, 0) in [:ok, :error]
      end
    end

    @tag :yecc_required
    test "parses MIB with various syntax types" do
      types = [
        "Integer32",
        "OCTET STRING",
        "OBJECT IDENTIFIER",
        "Counter32",
        "Counter64",
        "Gauge32",
        "TimeTicks",
        "IpAddress"
      ]

      for type <- types do
        mib = """
        TEST-MIB DEFINITIONS ::= BEGIN
        testObject OBJECT-TYPE
          SYNTAX #{type}
          MAX-ACCESS read-only
          STATUS current
          DESCRIPTION "Test"
          ::= { test 1 }
        END
        """

        result = Parser.parse(mib)
        assert is_tuple(result) and elem(result, 0) in [:ok, :error]
      end
    end

    @tag :yecc_required
    test "parser has known bug with index 10 (newline character)" do
      mib_path = Path.join(:code.priv_dir(:towerops), "mibs/ubnt/UBNT-MIB")
      {:ok, content} = File.read(mib_path)
      {:ok, parsed} = Parser.parse(content)

      definitions = Map.get(parsed, :definitions, [])

      # ubntAFLTU is defined as { ubntMIB 10 } in the MIB file
      # But the parser returns sub_index: nil because 10 is the newline character
      ubnt_afltu = Enum.find(definitions, &(Map.get(&1, :name) == "ubntAFLTU"))
      assert ubnt_afltu
      assert Map.get(ubnt_afltu, :parent) == "ubntMIB"
      assert Map.get(ubnt_afltu, :sub_index) == nil, "Known parser bug: index 10 not extracted"
    end
  end
end
