defmodule SnmpKit.SnmpMgr.TypesTest do
  use ExUnit.Case, async: true

  alias SnmpKit.SnmpMgr.Types

  describe "encode_value/2 with automatic type inference" do
    test "infers string type for printable binary" do
      assert {:ok, {:string, "Hello World"}} = Types.encode_value("Hello World")
    end

    test "infers string type for empty string" do
      assert {:ok, {:string, ""}} = Types.encode_value("")
    end

    test "infers octetString for non-printable binary" do
      binary = <<0, 1, 2, 3>>
      assert {:ok, {:octetString, ^binary}} = Types.encode_value(binary)
    end

    test "infers integer type for negative numbers" do
      assert {:ok, {:integer, -42}} = Types.encode_value(-42)
    end

    test "infers unsigned32 for positive integers under 2^32" do
      assert {:ok, {:unsigned32, 1000}} = Types.encode_value(1000)
      assert {:ok, {:unsigned32, 4_294_967_295}} = Types.encode_value(4_294_967_295)
    end

    test "infers counter64 for large positive integers" do
      assert {:ok, {:counter64, 4_294_967_296}} = Types.encode_value(4_294_967_296)
      assert {:ok, {:counter64, 9_999_999_999}} = Types.encode_value(9_999_999_999)
    end

    test "infers objectIdentifier for integer lists" do
      assert {:ok, {:objectIdentifier, [1, 3, 6, 1]}} = Types.encode_value([1, 3, 6, 1])
    end

    test "infers string for non-integer lists but fails encoding" do
      # Non-integer lists are inferred as :string but encode_with_inferred_type only handles binaries
      # This will raise FunctionClauseError
      assert_raise FunctionClauseError, fn ->
        Types.encode_value([1, 2, "a"])
      end
    end

    test "infers boolean type" do
      assert {:ok, {:boolean, true}} = Types.encode_value(true)
      assert {:ok, {:boolean, false}} = Types.encode_value(false)
    end

    test "infers ipAddress for valid IP tuple" do
      assert {:ok, {:ipAddress, {192, 168, 1, 1}}} = Types.encode_value({192, 168, 1, 1})
    end

    test "infers opaque for invalid IP tuple" do
      assert {:ok, {:opaque, {300, 400, 500, 600}}} = Types.encode_value({300, 400, 500, 600})
    end

    test "infers null for nil" do
      assert {:ok, {:null, :null}} = Types.encode_value(nil)
    end

    test "infers null for nil only" do
      assert {:ok, {:null, :null}} = Types.encode_value(nil)
    end

    test "special atoms require explicit type specification" do
      # Special atoms are inferred as :null but encode_with_inferred_type only handles nil
      # Use explicit type specification for these
      assert {:ok, {:null, :null}} = Types.encode_value(:undefined, type: :null)
      assert {:ok, {:null, :null}} = Types.encode_value(:noSuchObject, type: :null)
      assert {:ok, {:null, :null}} = Types.encode_value(:noSuchInstance, type: :null)
      assert {:ok, {:null, :null}} = Types.encode_value(:endOfMibView, type: :null)
    end

    test "infers opaque for unknown types" do
      value = %{key: "value"}
      assert {:ok, {:opaque, ^value}} = Types.encode_value(value)
    end
  end

  describe "encode_value/2 with explicit type specification" do
    test "encodes string type explicitly" do
      assert {:ok, {:string, "test"}} = Types.encode_value("test", type: :string)
    end

    test "encodes integer type explicitly" do
      assert {:ok, {:integer, 42}} = Types.encode_value(42, type: :integer)
    end

    test "encodes gauge32 type explicitly" do
      assert {:ok, {:gauge32, 100}} = Types.encode_value(100, type: :gauge32)
    end

    test "encodes counter32 type explicitly" do
      assert {:ok, {:counter32, 1000}} = Types.encode_value(1000, type: :counter32)
    end

    test "encodes counter64 type explicitly" do
      assert {:ok, {:counter64, 9_999_999_999}} = Types.encode_value(9_999_999_999, type: :counter64)
    end

    test "encodes unsigned32 type explicitly" do
      assert {:ok, {:unsigned32, 500}} = Types.encode_value(500, type: :unsigned32)
    end

    test "encodes timeticks type explicitly" do
      assert {:ok, {:timeticks, 12_345}} = Types.encode_value(12_345, type: :timeticks)
    end

    test "encodes ipAddress from string" do
      assert {:ok, {:ipAddress, {192, 168, 1, 1}}} =
               Types.encode_value("192.168.1.1", type: :ipAddress)
    end

    test "encodes ipAddress from tuple" do
      assert {:ok, {:ipAddress, {10, 0, 0, 1}}} =
               Types.encode_value({10, 0, 0, 1}, type: :ipAddress)
    end

    test "returns error for invalid IP address string" do
      assert {:error, {:invalid_ip_address, "999.999.999.999"}} =
               Types.encode_value("999.999.999.999", type: :ipAddress)
    end

    test "encodes objectIdentifier from string" do
      assert {:ok, {:objectIdentifier, [1, 3, 6, 1, 2, 1]}} =
               Types.encode_value("1.3.6.1.2.1", type: :objectIdentifier)
    end

    test "encodes objectIdentifier from list" do
      assert {:ok, {:objectIdentifier, [1, 3, 6, 1]}} =
               Types.encode_value([1, 3, 6, 1], type: :objectIdentifier)
    end

    test "returns error for invalid OID string" do
      assert {:error, _} = Types.encode_value("invalid.oid", type: :objectIdentifier)
    end

    test "returns error for non-integer OID list" do
      assert {:error, _} = Types.encode_value([1, 2, "a"], type: :objectIdentifier)
    end

    test "encodes octetString type explicitly" do
      binary = <<0, 1, 2>>
      assert {:ok, {:octetString, ^binary}} = Types.encode_value(binary, type: :octetString)
    end

    test "encodes boolean type explicitly" do
      assert {:ok, {:boolean, true}} = Types.encode_value(true, type: :boolean)
      assert {:ok, {:boolean, false}} = Types.encode_value(false, type: :boolean)
    end

    test "encodes opaque type explicitly" do
      value = %{test: "data"}
      assert {:ok, {:opaque, ^value}} = Types.encode_value(value, type: :opaque)
    end

    test "encodes null type explicitly" do
      assert {:ok, {:null, :null}} = Types.encode_value(nil, type: :null)
      assert {:ok, {:null, :null}} = Types.encode_value("anything", type: :null)
    end

    test "returns error for unsupported type conversions" do
      assert {:error, {:unsupported_type_conversion, "test", :gauge32}} =
               Types.encode_value("test", type: :gauge32)

      assert {:error, {:unsupported_type_conversion, -1, :counter32}} =
               Types.encode_value(-1, type: :counter32)

      assert {:error, {:unsupported_type_conversion, "test", :unknown_type}} =
               Types.encode_value("test", type: :unknown_type)
    end
  end

  describe "decode_value/1" do
    test "decodes string values" do
      assert Types.decode_value({:string, "hello"}) == "hello"
      assert Types.decode_value({:string, ""}) == ""
    end

    test "decodes integer values" do
      assert Types.decode_value({:integer, 42}) == 42
      assert Types.decode_value({:integer, -100}) == -100
    end

    test "decodes gauge32 values" do
      assert Types.decode_value({:gauge32, 1000}) == 1000
    end

    test "decodes counter32 values" do
      assert Types.decode_value({:counter32, 5000}) == 5000
    end

    test "decodes counter64 values" do
      assert Types.decode_value({:counter64, 9_999_999_999}) == 9_999_999_999
    end

    test "decodes unsigned32 values" do
      assert Types.decode_value({:unsigned32, 300}) == 300
    end

    test "decodes timeticks values" do
      assert Types.decode_value({:timeticks, 12_345}) == 12_345
    end

    test "decodes valid ipAddress tuples" do
      assert Types.decode_value({:ipAddress, {192, 168, 1, 1}}) == "192.168.1.1"
      assert Types.decode_value({:ipAddress, {10, 0, 0, 1}}) == "10.0.0.1"
      assert Types.decode_value({:ipAddress, {0, 0, 0, 0}}) == "0.0.0.0"
      assert Types.decode_value({:ipAddress, {255, 255, 255, 255}}) == "255.255.255.255"
    end

    test "returns error for invalid ipAddress tuples" do
      assert {:error, {:invalid_ip_address, {300, 400, 500, 600}}} =
               Types.decode_value({:ipAddress, {300, 400, 500, 600}})

      assert {:error, {:invalid_ip_address, {-1, 0, 0, 0}}} =
               Types.decode_value({:ipAddress, {-1, 0, 0, 0}})
    end

    test "decodes objectId values" do
      assert Types.decode_value({:objectId, [1, 3, 6, 1]}) == [1, 3, 6, 1]
    end

    test "decodes objectIdentifier values" do
      assert Types.decode_value({:objectIdentifier, [1, 3, 6, 1, 2, 1]}) == [1, 3, 6, 1, 2, 1]
    end

    test "decodes octetString values" do
      binary = <<0, 1, 2, 3>>
      assert Types.decode_value({:octetString, binary}) == binary
    end

    test "decodes boolean values" do
      assert Types.decode_value({:boolean, true}) == true
      assert Types.decode_value({:boolean, false}) == false
    end

    test "decodes opaque values" do
      value = %{test: "data"}
      assert Types.decode_value({:opaque, value}) == value
    end

    test "decodes null values" do
      assert Types.decode_value({:null, nil}) == nil
      assert Types.decode_value({:null, :null}) == nil
    end

    test "decodes SNMPv2c exception values" do
      assert Types.decode_value(:noSuchObject) == :no_such_object
      assert Types.decode_value(:noSuchInstance) == :no_such_instance
      assert Types.decode_value(:endOfMibView) == :end_of_mib_view
    end

    test "returns error for unknown SNMP types" do
      assert {:error, {:unknown_snmp_type, :unknown_type}} =
               Types.decode_value({:unknown_type, "value"})
    end

    test "passes through non-tuple values unchanged" do
      assert Types.decode_value("plain string") == "plain string"
      assert Types.decode_value(42) == 42
      assert Types.decode_value([1, 2, 3]) == [1, 2, 3]
    end
  end

  describe "infer_type/1" do
    test "infers string for printable binaries" do
      assert Types.infer_type("Hello World") == :string
      assert Types.infer_type("Test 123") == :string
      assert Types.infer_type("") == :string
    end

    test "infers octetString for non-printable binaries" do
      assert Types.infer_type(<<0, 1, 2, 3>>) == :octetString
      assert Types.infer_type(<<255, 254, 253>>) == :octetString
    end

    test "infers unsigned32 for small positive integers" do
      assert Types.infer_type(0) == :unsigned32
      assert Types.infer_type(1000) == :unsigned32
      assert Types.infer_type(4_294_967_295) == :unsigned32
    end

    test "infers counter64 for large positive integers" do
      assert Types.infer_type(4_294_967_296) == :counter64
      assert Types.infer_type(9_999_999_999) == :counter64
    end

    test "infers integer for negative integers" do
      assert Types.infer_type(-1) == :integer
      assert Types.infer_type(-1000) == :integer
    end

    test "infers objectIdentifier for integer lists" do
      assert Types.infer_type([1, 3, 6, 1]) == :objectIdentifier
      assert Types.infer_type([1]) == :objectIdentifier
    end

    test "infers string for non-integer lists" do
      assert Types.infer_type([1, 2, "a"]) == :string
      assert Types.infer_type(["a", "b"]) == :string
    end

    test "infers boolean for boolean values" do
      assert Types.infer_type(true) == :boolean
      assert Types.infer_type(false) == :boolean
    end

    test "infers ipAddress for valid IP tuples" do
      assert Types.infer_type({192, 168, 1, 1}) == :ipAddress
      assert Types.infer_type({0, 0, 0, 0}) == :ipAddress
      assert Types.infer_type({255, 255, 255, 255}) == :ipAddress
    end

    test "infers opaque for invalid IP tuples" do
      assert Types.infer_type({300, 400, 500, 600}) == :opaque
      assert Types.infer_type({-1, 0, 0, 0}) == :opaque
    end

    test "infers null for nil and special atoms" do
      assert Types.infer_type(nil) == :null
      assert Types.infer_type(:null) == :null
      assert Types.infer_type(:undefined) == :null
      assert Types.infer_type(:noSuchObject) == :null
      assert Types.infer_type(:noSuchInstance) == :null
      assert Types.infer_type(:endOfMibView) == :null
    end

    test "infers opaque for unknown types" do
      assert Types.infer_type(%{key: "value"}) == :opaque
      assert Types.infer_type({1, 2, 3, 4, 5}) == :opaque
      assert Types.infer_type(:some_atom) == :opaque
    end
  end

  describe "encode_value/2 edge cases" do
    test "handles boundary values for unsigned32" do
      assert {:ok, {:unsigned32, 0}} = Types.encode_value(0, type: :unsigned32)

      assert {:error, {:unsupported_type_conversion, -1, :unsigned32}} =
               Types.encode_value(-1, type: :unsigned32)
    end

    test "handles boundary values for counter32" do
      assert {:ok, {:counter32, 0}} = Types.encode_value(0, type: :counter32)

      assert {:error, {:unsupported_type_conversion, -1, :counter32}} =
               Types.encode_value(-1, type: :counter32)
    end

    test "handles boundary values for counter64" do
      assert {:ok, {:counter64, 0}} = Types.encode_value(0, type: :counter64)

      assert {:error, {:unsupported_type_conversion, -1, :counter64}} =
               Types.encode_value(-1, type: :counter64)
    end

    test "handles boundary values for gauge32" do
      assert {:ok, {:gauge32, 0}} = Types.encode_value(0, type: :gauge32)

      assert {:error, {:unsupported_type_conversion, -1, :gauge32}} =
               Types.encode_value(-1, type: :gauge32)
    end

    test "handles boundary values for timeticks" do
      assert {:ok, {:timeticks, 0}} = Types.encode_value(0, type: :timeticks)

      assert {:error, {:unsupported_type_conversion, -1, :timeticks}} =
               Types.encode_value(-1, type: :timeticks)
    end

    test "handles empty OID list" do
      assert {:ok, {:objectIdentifier, []}} = Types.encode_value([], type: :objectIdentifier)
    end

    test "handles single element OID list" do
      assert {:ok, {:objectIdentifier, [1]}} = Types.encode_value([1], type: :objectIdentifier)
    end

    test "handles IPv6-style addresses gracefully" do
      # :inet.parse_address actually supports IPv6, returns 8-tuple
      # But our encode_with_explicit_type only handles 4-tuples (IPv4)
      result = Types.encode_value("::1", type: :ipAddress)
      # Implementation accepts it because :inet.parse_address succeeds
      assert match?({:ok, {:ipAddress, _}}, result)
    end

    test "handles malformed IP addresses" do
      assert {:error, {:invalid_ip_address, "256.1.1.1"}} =
               Types.encode_value("256.1.1.1", type: :ipAddress)

      # :inet.parse_address may successfully parse "192.168.1" as 192.168.0.1
      result = Types.encode_value("192.168.1", type: :ipAddress)
      assert match?({:ok, _}, result) or match?({:error, {:invalid_ip_address, "192.168.1"}}, result)

      assert {:error, {:invalid_ip_address, "invalid"}} =
               Types.encode_value("invalid", type: :ipAddress)
    end

    test "handles UTF-8 strings correctly" do
      assert {:ok, {:string, "Hello 世界"}} = Types.encode_value("Hello 世界")
      assert {:ok, {:string, "Ñoño"}} = Types.encode_value("Ñoño")
    end
  end

  describe "roundtrip encoding and decoding" do
    test "roundtrips string values" do
      {:ok, encoded} = Types.encode_value("test")
      assert Types.decode_value(encoded) == "test"
    end

    test "roundtrips integer values" do
      {:ok, encoded} = Types.encode_value(42, type: :integer)
      assert Types.decode_value(encoded) == 42
    end

    test "roundtrips IP addresses" do
      {:ok, encoded} = Types.encode_value("192.168.1.1", type: :ipAddress)
      assert Types.decode_value(encoded) == "192.168.1.1"
    end

    test "roundtrips OIDs" do
      {:ok, encoded} = Types.encode_value([1, 3, 6, 1, 2, 1])
      decoded = Types.decode_value(encoded)
      assert decoded == [1, 3, 6, 1, 2, 1]
    end

    test "roundtrips boolean values" do
      {:ok, encoded_true} = Types.encode_value(true)
      assert Types.decode_value(encoded_true) == true

      {:ok, encoded_false} = Types.encode_value(false)
      assert Types.decode_value(encoded_false) == false
    end

    test "roundtrips null values" do
      {:ok, encoded} = Types.encode_value(nil)
      assert Types.decode_value(encoded) == nil
    end
  end
end
