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

  alias SnmpKit.SnmpLib.MIB.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))
      assert is_binary(Error.format(error, verbose: true))
      assert is_binary(Error.format(error, []))
    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 is_binary(formatted)
      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 is_binary(formatted)
      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 is_binary(formatted)
    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)
    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 is_binary(formatted)
    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 is_binary(error.message)
        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 is_binary(Error.format(error3))
    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)
    end

    test "import_error without options" do
      error = Error.new(:import_error)
      assert is_binary(error.message)
    end

    test "duplicate_definition without name" do
      error = Error.new(:duplicate_definition)
      assert is_binary(error.message)
    end
  end
end
