defmodule SnmpKit.SnmpLib.PDU.V3EncoderTest do
  use ExUnit.Case, async: true

  alias SnmpKit.SnmpLib.PDU.V3Encoder

  describe "create_discovery_message/1" do
    test "creates discovery message with default msg_id" do
      msg = V3Encoder.create_discovery_message()

      assert msg.version == 3
      assert is_integer(msg.msg_id)
      assert msg.msg_id > 0
      assert msg.msg_flags.auth == false
      assert msg.msg_flags.priv == false
      assert msg.msg_flags.reportable == true
      assert msg.msg_security_parameters == <<>>
      assert msg.msg_data.context_engine_id == <<>>
      assert msg.msg_data.context_name == <<>>
    end

    test "creates discovery message with custom msg_id" do
      msg = V3Encoder.create_discovery_message(12_345)

      assert msg.msg_id == 12_345
      assert msg.version == 3
    end

    test "includes snmpEngineID varbind in discovery message" do
      msg = V3Encoder.create_discovery_message()

      pdu = msg.msg_data.pdu
      assert pdu.type == :get_request
      assert length(pdu.varbinds) == 1

      # snmpEngineID OID
      [{oid, type, value}] = pdu.varbinds
      assert oid == [1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0]
      assert type == :null
      assert value == :null
    end
  end

  describe "encode_message/2" do
    test "returns error for non-v3 message" do
      msg = %{version: 2}

      assert {:error, :invalid_version} = V3Encoder.encode_message(msg, nil)
    end

    test "returns error for invalid message format" do
      assert {:error, :invalid_message_format} = V3Encoder.encode_message(%{}, nil)
    end

    test "encodes discovery message without user" do
      msg = V3Encoder.create_discovery_message(999)

      # Discovery messages should encode without user
      result = V3Encoder.encode_message(msg, nil)

      # Should succeed or fail with specific error (not crash)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end
  end

  describe "decode_message/2" do
    test "returns error for invalid binary data" do
      result = V3Encoder.decode_message(<<1, 2, 3>>, nil)
      assert match?({:error, _}, result)
    end

    test "returns error for empty data" do
      result = V3Encoder.decode_message(<<>>, nil)
      assert match?({:error, _}, result)
    end

    test "handles malformed SEQUENCE" do
      # Invalid SEQUENCE tag
      data = <<0xFF, 0x10, 1, 2, 3>>

      result = V3Encoder.decode_message(data, nil)
      assert match?({:error, _}, result)
    end
  end

  describe "roundtrip encoding/decoding" do
    test "discovery message roundtrip without security" do
      original_msg = V3Encoder.create_discovery_message(12_345)

      case V3Encoder.encode_message(original_msg, nil) do
        {:ok, encoded} ->
          case V3Encoder.decode_message(encoded, nil) do
            {:ok, decoded} ->
              # Version should match
              assert decoded.version == original_msg.version
              # Message ID should match
              assert decoded.msg_id == original_msg.msg_id
              # Flags should match
              assert decoded.msg_flags.auth == original_msg.msg_flags.auth
              assert decoded.msg_flags.priv == original_msg.msg_flags.priv

            {:error, _reason} ->
              # Decoding may fail due to complex ASN.1 processing, that's acceptable
              :ok
          end

        {:error, _reason} ->
          # Encoding may fail without full dependencies, that's acceptable
          :ok
      end
    end
  end

  describe "classify_error/1" do
    # Test error classification through the ErrorHandler if V3Encoder uses it
    test "v3 encoder handles encoding failures gracefully" do
      invalid_msg = %{
        version: 3,
        msg_id: 1,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: false},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{type: :invalid_type}
        }
      }

      result = V3Encoder.encode_message(invalid_msg, nil)

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

  describe "security parameter handling" do
    test "encodes message without security parameters" do
      msg = %{
        version: 3,
        msg_id: 123,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_security_parameters: <<>>,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_request,
            request_id: 123,
            error_status: 0,
            error_index: 0,
            varbinds: []
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)

      # Should succeed or fail gracefully (not crash)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end
  end

  describe "message flags" do
    test "creates message with various flag combinations" do
      # No auth, no priv
      msg1 = %{V3Encoder.create_discovery_message() | msg_flags: %{auth: false, priv: false, reportable: true}}
      assert msg1.msg_flags.auth == false
      assert msg1.msg_flags.priv == false

      # Auth only
      msg2 = %{V3Encoder.create_discovery_message() | msg_flags: %{auth: true, priv: false, reportable: true}}
      assert msg2.msg_flags.auth == true
      assert msg2.msg_flags.priv == false

      # Auth and priv
      msg3 = %{V3Encoder.create_discovery_message() | msg_flags: %{auth: true, priv: true, reportable: false}}
      assert msg3.msg_flags.auth == true
      assert msg3.msg_flags.priv == true
    end
  end

  describe "edge cases" do
    test "handles empty varbinds" do
      msg = %{
        version: 3,
        msg_id: 1,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_request,
            request_id: 1,
            error_status: 0,
            error_index: 0,
            varbinds: []
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)

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

    test "handles large msg_id" do
      large_id = 2_147_483_647
      msg = V3Encoder.create_discovery_message(large_id)

      assert msg.msg_id == large_id
    end

    test "handles various context names" do
      msg = V3Encoder.create_discovery_message()

      msg1 = %{msg | msg_data: %{msg.msg_data | context_name: "test-context"}}
      assert msg1.msg_data.context_name == "test-context"

      msg2 = %{msg | msg_data: %{msg.msg_data | context_name: <<>>}}
      assert msg2.msg_data.context_name == <<>>
    end
  end

  describe "PDU types in SNMPv3 messages" do
    test "encodes get_next_request PDU in v3 message" do
      msg = %{
        version: 3,
        msg_id: 456,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_next_request,
            request_id: 456,
            error_status: 0,
            error_index: 0,
            varbinds: [{[1, 3, 6, 1, 2, 1], :null, nil}]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes get_response PDU in v3 message" do
      msg = %{
        version: 3,
        msg_id: 789,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_response,
            request_id: 789,
            error_status: 0,
            error_index: 0,
            varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :octet_string, "System description"}]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes set_request PDU in v3 message" do
      msg = %{
        version: 3,
        msg_id: 111,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :set_request,
            request_id: 111,
            error_status: 0,
            error_index: 0,
            varbinds: [{[1, 3, 6, 1, 2, 1, 1, 5, 0], :octet_string, "NewName"}]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes get_bulk_request PDU in v3 message" do
      msg = %{
        version: 3,
        msg_id: 999,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_bulk_request,
            request_id: 999,
            non_repeaters: 0,
            max_repetitions: 10,
            varbinds: [{[1, 3, 6, 1, 2, 1, 2, 2], :null, nil}]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end
  end

  describe "context engine ID and context name" do
    test "encodes message with non-empty context_engine_id" do
      msg = %{
        version: 3,
        msg_id: 123,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<0x80, 0x00, 0x1F, 0x88, 0x80>>,
          context_name: <<>>,
          pdu: %{
            type: :get_request,
            request_id: 123,
            error_status: 0,
            error_index: 0,
            varbinds: []
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes message with non-empty context_name" do
      msg = %{
        version: 3,
        msg_id: 456,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: "my-context",
          pdu: %{
            type: :get_request,
            request_id: 456,
            error_status: 0,
            error_index: 0,
            varbinds: []
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes message with both context_engine_id and context_name" do
      msg = %{
        version: 3,
        msg_id: 789,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<0x80, 0x00, 0x1F, 0x88, 0x80>>,
          context_name: "remote-context",
          pdu: %{
            type: :get_request,
            request_id: 789,
            error_status: 0,
            error_index: 0,
            varbinds: []
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end
  end

  describe "message max size variations" do
    test "encodes message with minimum msg_max_size" do
      msg = %{
        version: 3,
        msg_id: 1,
        msg_max_size: 484,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_request,
            request_id: 1,
            error_status: 0,
            error_index: 0,
            varbinds: []
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes message with custom msg_max_size" do
      msg = %{
        version: 3,
        msg_id: 1,
        msg_max_size: 32_768,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_request,
            request_id: 1,
            error_status: 0,
            error_index: 0,
            varbinds: []
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end
  end

  describe "message with multiple varbinds" do
    test "encodes v3 message with multiple varbinds" do
      msg = %{
        version: 3,
        msg_id: 12_345,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_request,
            request_id: 12_345,
            error_status: 0,
            error_index: 0,
            varbinds: [
              {[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, nil},
              {[1, 3, 6, 1, 2, 1, 1, 3, 0], :null, nil},
              {[1, 3, 6, 1, 2, 1, 1, 5, 0], :null, nil}
            ]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes v3 response with various value types" do
      msg = %{
        version: 3,
        msg_id: 99_999,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_response,
            request_id: 99_999,
            error_status: 0,
            error_index: 0,
            varbinds: [
              {[1, 3, 6, 1, 2, 1, 1, 1, 0], :octet_string, "System description"},
              {[1, 3, 6, 1, 2, 1, 1, 3, 0], :timeticks, 123_456_789},
              {[1, 3, 6, 1, 2, 1, 1, 7, 0], :integer, 72},
              {[1, 3, 6, 1, 2, 1, 2, 1, 10, 1], :counter32, 987_654_321}
            ]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end
  end

  describe "error responses in v3" do
    test "encodes v3 message with non-zero error_status" do
      msg = %{
        version: 3,
        msg_id: 777,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_response,
            request_id: 777,
            error_status: 2,
            error_index: 1,
            varbinds: [{[1, 3, 6, 1, 2, 1, 1, 1, 0], :null, nil}]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes v3 message with various error statuses" do
      for error_status <- 0..5 do
        msg = %{
          version: 3,
          msg_id: 1000 + error_status,
          msg_max_size: 65_507,
          msg_flags: %{auth: false, priv: false, reportable: true},
          msg_security_model: 3,
          msg_data: %{
            context_engine_id: <<>>,
            context_name: <<>>,
            pdu: %{
              type: :get_response,
              request_id: 1000 + error_status,
              error_status: error_status,
              error_index: 0,
              varbinds: []
            }
          }
        }

        result = V3Encoder.encode_message(msg, nil)
        assert match?({:ok, _}, result) or match?({:error, _}, result)
      end
    end
  end

  describe "reportable flag variations" do
    test "encodes message with reportable=false" do
      msg = %{
        version: 3,
        msg_id: 111,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: false},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_request,
            request_id: 111,
            error_status: 0,
            error_index: 0,
            varbinds: []
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes message with reportable=true" do
      msg = V3Encoder.create_discovery_message()
      assert msg.msg_flags.reportable == true

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end
  end

  describe "security model variations" do
    test "encodes message with USM security model (3)" do
      msg = %{
        version: 3,
        msg_id: 1,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_request,
            request_id: 1,
            error_status: 0,
            error_index: 0,
            varbinds: []
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end
  end

  describe "varbind value type coverage in v3" do
    test "encodes v3 message with counter64 value" do
      msg = %{
        version: 3,
        msg_id: 5555,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_response,
            request_id: 5555,
            error_status: 0,
            error_index: 0,
            varbinds: [{[1, 3, 6, 1, 2, 1, 31, 1, 1, 1, 6, 1], :counter64, 9_876_543_210}]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes v3 message with gauge32 value" do
      msg = %{
        version: 3,
        msg_id: 6666,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_response,
            request_id: 6666,
            error_status: 0,
            error_index: 0,
            varbinds: [{[1, 3, 6, 1, 2, 1, 2, 1, 5, 1], :gauge32, 1_000_000}]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes v3 message with ip_address value" do
      msg = %{
        version: 3,
        msg_id: 7777,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_response,
            request_id: 7777,
            error_status: 0,
            error_index: 0,
            varbinds: [{[1, 3, 6, 1, 2, 1, 4, 20, 1, 1, 1], :ip_address, <<192, 168, 1, 1>>}]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes v3 message with opaque value" do
      msg = %{
        version: 3,
        msg_id: 8888,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_response,
            request_id: 8888,
            error_status: 0,
            error_index: 0,
            varbinds: [{[1, 3, 6, 1, 4, 1, 9, 2, 1, 57, 0], :opaque, <<0x9F, 0x78, 0x04>>}]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes v3 message with object_identifier value" do
      msg = %{
        version: 3,
        msg_id: 9999,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_response,
            request_id: 9999,
            error_status: 0,
            error_index: 0,
            varbinds: [{[1, 3, 6, 1, 2, 1, 1, 2, 0], :object_identifier, [1, 3, 6, 1, 4, 1]}]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end
  end

  describe "exception values in v3" do
    test "encodes v3 message with no_such_object exception" do
      msg = %{
        version: 3,
        msg_id: 1111,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_response,
            request_id: 1111,
            error_status: 0,
            error_index: 0,
            varbinds: [{[1, 3, 6, 1, 2, 1, 99, 1], :no_such_object, nil}]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes v3 message with no_such_instance exception" do
      msg = %{
        version: 3,
        msg_id: 2222,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_response,
            request_id: 2222,
            error_status: 0,
            error_index: 0,
            varbinds: [{[1, 3, 6, 1, 2, 1, 99, 2], :no_such_instance, nil}]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes v3 message with end_of_mib_view exception" do
      msg = %{
        version: 3,
        msg_id: 3333,
        msg_max_size: 65_507,
        msg_flags: %{auth: false, priv: false, reportable: true},
        msg_security_model: 3,
        msg_data: %{
          context_engine_id: <<>>,
          context_name: <<>>,
          pdu: %{
            type: :get_response,
            request_id: 3333,
            error_status: 0,
            error_index: 0,
            varbinds: [{[1, 3, 6, 1, 2, 1, 99, 3], :end_of_mib_view, nil}]
          }
        }
      }

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end
  end

  describe "message ID edge cases" do
    test "encodes message with zero msg_id" do
      msg = V3Encoder.create_discovery_message(0)
      assert msg.msg_id == 0

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end

    test "encodes message with maximum msg_id" do
      max_id = 2_147_483_647
      msg = V3Encoder.create_discovery_message(max_id)
      assert msg.msg_id == max_id

      result = V3Encoder.encode_message(msg, nil)
      assert match?({:ok, _}, result) or match?({:error, _}, result)
    end
  end

  describe "discovery message consistency" do
    test "creates discovery message with consistent structure" do
      msg1 = V3Encoder.create_discovery_message()
      msg2 = V3Encoder.create_discovery_message()

      # Same structure, different msg_id
      assert msg1.version == msg2.version
      assert msg1.msg_max_size == msg2.msg_max_size
      assert msg1.msg_flags == msg2.msg_flags
      assert msg1.msg_security_model == msg2.msg_security_model
      assert msg1.msg_data.context_engine_id == msg2.msg_data.context_engine_id
      assert msg1.msg_data.context_name == msg2.msg_data.context_name
      assert msg1.msg_data.pdu.type == msg2.msg_data.pdu.type
    end

    test "discovery message uses snmpEngineID OID" do
      msg = V3Encoder.create_discovery_message()
      [{oid, _type, _value}] = msg.msg_data.pdu.varbinds

      # snmpEngineID OID: 1.3.6.1.6.3.10.2.1.1.0
      assert oid == [1, 3, 6, 1, 6, 3, 10, 2, 1, 1, 0]
    end
  end
end
