defmodule SnmpKit.SnmpLib.MIB.ErrorTest do use ExUnit.Case, async: true alias SnmpKit.SnmpLib.MIB.Error doctest Error describe "new/2" do test "creates error with type and message" do error = Error.new(:syntax_error) assert error.type == :syntax_error assert error.message == "Syntax error" assert error.line == nil assert error.column == nil assert error.context == %{} assert error.suggestions == [] end test "creates error with line and column" do error = Error.new(:semantic_error, line: 42, column: 10) assert error.type == :semantic_error assert error.line == 42 assert error.column == 10 end test "creates error with context" do context = %{file: "test.mib", token: "ACCESS"} error = Error.new(:type_error, context: context) assert error.context == context end test "generates message for unexpected_token error" do error = Error.new(:unexpected_token, expected: :max_access, actual: :access) assert error.message =~ "Expected" assert error.message =~ "max_access" assert error.message =~ "access" end test "generates message for unexpected_token with value" do error = Error.new(:unexpected_token, expected: :semicolon, actual: :identifier, value: "test") assert error.message =~ "test" end test "generates message for unexpected_eof" do error = Error.new(:unexpected_eof, expected: "closing brace") assert error.message == "Unexpected end of file, expected closing brace" end test "generates message for unterminated_string" do error = Error.new(:unterminated_string) assert error.message == "Unterminated string literal" end test "generates message for invalid_number" do error = Error.new(:invalid_number, value: "12.34.56") assert error.message == "Invalid number format: '12.34.56'" end test "generates message for invalid_identifier" do error = Error.new(:invalid_identifier, value: "1invalid") assert error.message == "Invalid identifier: '1invalid'" end test "generates message for file_not_found" do error = Error.new(:file_not_found, file: "/path/to/missing.mib") assert error.message == "File not found: /path/to/missing.mib" end test "generates message for import_error" do error = Error.new(:import_error, symbol: "sysDescr", from_module: "SNMPv2-MIB") assert error.message == "Cannot import 'sysDescr' from 'SNMPv2-MIB'" end test "generates message for duplicate_definition" do error = Error.new(:duplicate_definition, name: "testObject") assert error.message == "Duplicate definition of 'testObject'" end test "generates generic message for unknown error type" do error = Error.new(:constraint_error) assert error.message == "Constraint error" end test "generates suggestions for unexpected_token MAX-ACCESS" do error = Error.new(:unexpected_token, expected: :max_access, actual: :access) refute Enum.empty?(error.suggestions) assert Enum.any?(error.suggestions, &String.contains?(&1, "MAX-ACCESS")) end test "generates suggestions for unexpected_token mandatory/current" do error = Error.new(:unexpected_token, expected: :current, actual: :mandatory) refute Enum.empty?(error.suggestions) assert Enum.any?(error.suggestions, &String.contains?(&1, "deprecated")) end test "generates suggestions for unexpected_token OBJECT-TYPE" do error = Error.new(:unexpected_token, expected: :object_type, actual: :object_identity) refute Enum.empty?(error.suggestions) assert Enum.any?(error.suggestions, &String.contains?(&1, "OBJECT-TYPE")) end test "generates suggestions for unterminated_string" do error = Error.new(:unterminated_string) refute Enum.empty?(error.suggestions) assert Enum.any?(error.suggestions, &String.contains?(&1, "quote")) end test "generates suggestions for file_not_found with RFC file" do error = Error.new(:file_not_found, file: "RFC1213-MIB.txt") refute Enum.empty?(error.suggestions) assert Enum.any?(error.suggestions, &String.contains?(&1, "naming conventions")) end test "generates suggestions for file_not_found with MIB file" do error = Error.new(:file_not_found, file: "TEST-MIB.mib") refute Enum.empty?(error.suggestions) assert Enum.any?(error.suggestions, &String.contains?(&1, "extension")) end test "generates suggestions for file_not_found without RFC/MIB" do error = Error.new(:file_not_found, file: "/random/file.txt") assert length(error.suggestions) == 1 assert List.first(error.suggestions) =~ "file path is correct" end test "generates suggestions for import_error" do error = Error.new(:import_error, symbol: "test", from_module: "TEST-MIB") refute Enum.empty?(error.suggestions) assert Enum.any?(error.suggestions, &String.contains?(&1, "import")) end test "generates no suggestions for unknown error types" do error = Error.new(:semantic_error) assert error.suggestions == [] end end describe "format/2" do test "formats error without location" do error = Error.new(:syntax_error) formatted = Error.format(error) assert formatted =~ "Syntax error" end test "formats error with line only" do error = Error.new(:type_error, line: 42) formatted = Error.format(error) assert formatted =~ "Line 42" end test "formats error with column only" do error = Error.new(:semantic_error, column: 10) formatted = Error.format(error) assert formatted =~ "Column 10" end test "formats error with line and column" do error = Error.new(:unexpected_token, expected: :semicolon, actual: :identifier, line: 42, column: 10) formatted = Error.format(error) assert formatted =~ "Line 42, column 10" assert formatted =~ "Expected" end test "formats error with suggestions" do error = Error.new(:unterminated_string, line: 5) formatted = Error.format(error) assert formatted =~ "Suggestions:" assert formatted =~ "quote" end test "formats error without suggestions" do error = Error.new(:semantic_error, line: 10) formatted = Error.format(error) refute formatted =~ "Suggestions:" end test "formats multiple suggestions" do error = Error.new(:import_error, symbol: "test", from_module: "TEST") formatted = Error.format(error) assert formatted =~ "Suggestions:" # Count bullets bullet_count = formatted |> String.split("•") |> length() |> Kernel.-(1) assert bullet_count == length(error.suggestions) end test "accepts format options" do error = Error.new(:syntax_error, line: 1) # Should not crash with options assert is_binary(Error.format(error, color: true)) and byte_size(Error.format(error, color: true)) > 0 assert is_binary(Error.format(error, verbose: true)) and byte_size(Error.format(error, verbose: true)) > 0 assert is_binary(Error.format(error, [])) and byte_size(Error.format(error, [])) > 0 end test "formats unexpected_token error with custom message" do error = Error.new(:unexpected_token, message: "Custom error message", line: 5) formatted = Error.format(error) assert formatted =~ "Custom error message" assert formatted =~ "Line 5" end test "formats unexpected_token with nil values" do error = Error.new(:unexpected_token, expected: nil, actual: nil) formatted = Error.format(error) assert byte_size(formatted) > 0 assert formatted =~ "unknown" end test "formats unexpected_token with empty string values" do error = Error.new(:unexpected_token, expected: "", actual: "") formatted = Error.format(error) assert byte_size(formatted) > 0 assert formatted =~ "unknown" end test "formats unexpected_token with end of input" do error = Error.new(:unexpected_token, expected: :semicolon, actual: "") formatted = Error.format(error) # Empty string actual is converted to "unknown" by format_token_value assert formatted =~ "unknown" end test "formats unexpected_token with nil actual" do error = Error.new(:unexpected_token, expected: :identifier, actual: nil) formatted = Error.format(error) # nil actual is converted to "unknown" by format_token_value assert formatted =~ "unknown" end test "formats unexpected_token with various atom values" do error = Error.new(:unexpected_token, expected: :keyword, actual: :identifier) formatted = Error.format(error) assert formatted =~ "keyword" assert formatted =~ "identifier" end test "formats unexpected_token with binary values" do error = Error.new(:unexpected_token, expected: "semicolon", actual: "comma") formatted = Error.format(error) assert formatted =~ "semicolon" assert formatted =~ "comma" end test "formats unexpected_token with mixed value types" do error = Error.new(:unexpected_token, expected: :keyword, actual: "string") formatted = Error.format(error) assert formatted =~ "keyword" assert formatted =~ "string" end test "formats unexpected_token with non-standard values" do error = Error.new(:unexpected_token, expected: 123, actual: [:list]) formatted = Error.format(error) # Should use inspect for non-atom, non-binary values assert byte_size(formatted) > 0 end end describe "error struct" do test "has correct structure" do error = %Error{ type: :syntax_error, message: "test", line: 1, column: 2, context: %{key: "value"}, suggestions: ["suggestion"] } assert error.type == :syntax_error assert error.message == "test" assert error.line == 1 assert error.column == 2 assert error.context == %{key: "value"} assert error.suggestions == ["suggestion"] end test "suggestions default to empty list" do error = %Error{type: :test, message: "test"} assert error.suggestions == [] end end describe "edge cases" do test "handles nil options gracefully" do error = Error.new(:syntax_error, nil) assert error.type == :syntax_error assert is_binary(error.message) and byte_size(error.message) > 0 end test "handles empty options list" do error = Error.new(:semantic_error, []) assert error.type == :semantic_error assert error.context == %{} end test "handles error with all fields nil" do error = Error.new(:type_error, line: nil, column: nil, context: nil) formatted = Error.format(error) assert byte_size(formatted) > 0 end test "generates messages for all supported error types" do error_types = [ :syntax_error, :semantic_error, :import_error, :type_error, :constraint_error, :duplicate_definition, :file_not_found, :unterminated_string, :unexpected_token, :unexpected_eof, :invalid_number, :invalid_identifier ] for type <- error_types do error = Error.new(type) assert byte_size(error.message) > 0 end end test "handles context with various data types" do contexts = [ %{string: "value"}, %{integer: 42}, %{atom: :test}, %{list: [1, 2, 3]}, %{nested: %{key: "value"}}, %{} ] for context <- contexts do error = Error.new(:test_error, context: context) assert error.context == context end end test "handles line and column edge values" do # Large line numbers error1 = Error.new(:test, line: 999_999) assert Error.format(error1) =~ "999999" # Zero line/column error2 = Error.new(:test, line: 0, column: 0) formatted2 = Error.format(error2) assert formatted2 =~ "Line 0" assert formatted2 =~ "column 0" # Negative values (shouldn't happen but should handle) error3 = Error.new(:test, line: -1, column: -5) assert byte_size(Error.format(error3)) > 0 end end describe "message generation variations" do test "unexpected_token with only expected" do error = Error.new(:unexpected_token, expected: :semicolon) assert error.message =~ "semicolon" end test "unexpected_token with only actual" do error = Error.new(:unexpected_token, actual: :comma) assert error.message =~ "comma" end test "unexpected_token with all fields" do error = Error.new(:unexpected_token, expected: :semicolon, actual: :comma, value: ",", line: 10, column: 5 ) assert error.message =~ "semicolon" assert error.message =~ "comma" assert error.message =~ "," end test "file_not_found without file option" do error = Error.new(:file_not_found) assert is_binary(error.message) and byte_size(error.message) > 0 end test "import_error without options" do error = Error.new(:import_error) assert is_binary(error.message) and byte_size(error.message) > 0 end test "duplicate_definition without name" do error = Error.new(:duplicate_definition) assert is_binary(error.message) and byte_size(error.message) > 0 end end end