[CI] Add Java pairing OnNetworkLong test (#23816)

diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 23c0345..1154185 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -462,6 +462,18 @@
                       --target linux-x64-java-matter-controller \
                       build \
                    "
+            - name: Run Tests
+              timeout-minutes: 65
+              run: |
+                  scripts/run_in_build_env.sh \
+                  './scripts/tests/run_java_test.py \
+                     --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app \
+                     --app-args "--discriminator 3840 --interface-id -1" \
+                     --tool-path out/linux-x64-java-matter-controller \
+                     --tool-cluster "pairing" \
+                     --tool-args "--nodeid 1 --setup-payload 20202021 --discriminator 3840 -t 1000" \
+                     --factoryreset \
+                  '
             - name: Uploading core files
               uses: actions/upload-artifact@v3
               if: ${{ failure() && !env.ACT }}
diff --git a/examples/java-matter-controller/java/src/com/matter/controller/commands/common/CommandManager.java b/examples/java-matter-controller/java/src/com/matter/controller/commands/common/CommandManager.java
index 8a7110a..fb6d9cc 100644
--- a/examples/java-matter-controller/java/src/com/matter/controller/commands/common/CommandManager.java
+++ b/examples/java-matter-controller/java/src/com/matter/controller/commands/common/CommandManager.java
@@ -97,6 +97,7 @@
       command.initArguments(temp.length, temp);

       command.run();

     } catch (IllegalArgumentException e) {

+      System.out.println("Run command failed with exception: " + e.getMessage());

       showCommand(args[0], command);

     } catch (Exception e) {

       System.out.println("Run command failed with exception: " + e.getMessage());

diff --git a/scripts/tests/java/base.py b/scripts/tests/java/base.py
new file mode 100755
index 0000000..1417db0
--- /dev/null
+++ b/scripts/tests/java/base.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+
+#
+#    Copyright (c) 2022 Project CHIP Authors
+#    All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+
+# Commissioning test.
+import logging
+import os
+import sys
+import queue
+import datetime
+import asyncio
+import threading
+import typing
+import time
+import subprocess
+from colorama import Fore, Style
+
+
+def EnqueueLogOutput(fp, tag, q):
+    for line in iter(fp.readline, b''):
+        timestamp = time.time()
+        if len(line) > len('[1646290606.901990]') and line[0:1] == b'[':
+            try:
+                timestamp = float(line[1:18].decode())
+                line = line[19:]
+            except Exception as ex:
+                pass
+        sys.stdout.buffer.write(
+            (f"[{datetime.datetime.fromtimestamp(timestamp).isoformat(sep=' ')}]").encode() + tag + line)
+        sys.stdout.flush()
+    fp.close()
+
+
+def RedirectQueueThread(fp, tag, queue) -> threading.Thread:
+    log_queue_thread = threading.Thread(target=EnqueueLogOutput, args=(
+        fp, tag, queue))
+    log_queue_thread.start()
+    return log_queue_thread
+
+
+def DumpProgramOutputToQueue(thread_list: typing.List[threading.Thread], tag: str, process: subprocess.Popen, queue: queue.Queue):
+    thread_list.append(RedirectQueueThread(process.stdout,
+                                           (f"[{tag}][{Fore.YELLOW}STDOUT{Style.RESET_ALL}]").encode(), queue))
+    thread_list.append(RedirectQueueThread(process.stderr,
+                                           (f"[{tag}][{Fore.RED}STDERR{Style.RESET_ALL}]").encode(), queue))
diff --git a/scripts/tests/java/commissioning_test.py b/scripts/tests/java/commissioning_test.py
new file mode 100755
index 0000000..afd132c
--- /dev/null
+++ b/scripts/tests/java/commissioning_test.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+
+#
+#    Copyright (c) 2022 Project CHIP Authors
+#    All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+
+# Commissioning test.
+import logging
+import os
+import sys
+import asyncio
+import queue
+import subprocess
+import threading
+import typing
+from optparse import OptionParser
+from colorama import Fore, Style
+from java.base import DumpProgramOutputToQueue
+
+
+class CommissioningTest:
+    def __init__(self, thread_list: typing.List[threading.Thread], queue: queue.Queue, cmd: [], args: str):
+        self.thread_list = thread_list
+        self.queue = queue
+        self.command = cmd
+
+        optParser = OptionParser()
+        optParser.add_option(
+            "-t",
+            "--timeout",
+            action="store",
+            dest="testTimeout",
+            default='200',
+            type='str',
+            help="The program will return with timeout after specified seconds.",
+            metavar="<timeout-second>",
+        )
+        optParser.add_option(
+            "-a",
+            "--address",
+            action="store",
+            dest="deviceAddress",
+            default='',
+            type='str',
+            help="Address of the device",
+            metavar="<device-addr>",
+        )
+        optParser.add_option(
+            "--setup-payload",
+            action="store",
+            dest="setupPayload",
+            default='',
+            type='str',
+            help="Setup Payload (manual pairing code or QR code content)",
+            metavar="<setup-payload>"
+        )
+        optParser.add_option(
+            "--nodeid",
+            action="store",
+            dest="nodeid",
+            default='1',
+            type='str',
+            help="The Node ID issued to the device",
+            metavar="<nodeid>"
+        )
+        optParser.add_option(
+            "--discriminator",
+            action="store",
+            dest="discriminator",
+            default='3840',
+            type='str',
+            help="Discriminator of the device",
+            metavar="<nodeid>"
+        )
+        optParser.add_option(
+            "-p",
+            "--paa-trust-store-path",
+            action="store",
+            dest="paaTrustStorePath",
+            default='',
+            type='str',
+            help="Path that contains valid and trusted PAA Root Certificates.",
+            metavar="<paa-trust-store-path>"
+        )
+
+        (options, remainingArgs) = optParser.parse_args(args.split())
+
+        self.nodeid = options.nodeid
+        self.setupPayload = options.setupPayload
+        self.discriminator = options.discriminator
+        self.testTimeout = options.testTimeout
+
+        logging.basicConfig(level=logging.INFO)
+
+    def TestOnnetworkLong(self, nodeid, setuppin, discriminator, timeout):
+        java_command = self.command + ['pairing', 'onnetwork-long', nodeid, setuppin, discriminator, timeout]
+        print(java_command)
+        logging.info(f"Execute: {java_command}")
+        java_process = subprocess.Popen(
+            java_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        DumpProgramOutputToQueue(self.thread_list, Fore.GREEN + "JAVA " + Style.RESET_ALL, java_process, self.queue)
+        return java_process.wait()
+
+    def RunTest(self):
+        logging.info("Testing onnetwork-long pairing")
+        java_exit_code = self.TestOnnetworkLong(self.nodeid, self.setupPayload, self.discriminator, self.testTimeout)
+        if java_exit_code != 0:
+            logging.error("Testing onnetwork-long pairing failed with error %r" % java_exit_code)
+            return java_exit_code
+
+        # Testing complete without errors
+        return 0
diff --git a/scripts/tests/run_java_test.py b/scripts/tests/run_java_test.py
new file mode 100755
index 0000000..4ca163a
--- /dev/null
+++ b/scripts/tests/run_java_test.py
@@ -0,0 +1,114 @@
+#!/usr/bin/env -S python3 -B
+
+# Copyright (c) 2022 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import click
+import coloredlogs
+import logging
+import os
+import pathlib
+import pty
+import queue
+import re
+import shlex
+import signal
+import subprocess
+import sys
+from java.base import DumpProgramOutputToQueue
+from java.commissioning_test import CommissioningTest
+from colorama import Fore, Style
+
+
+@click.command()
+@click.option("--app", type=click.Path(exists=True), default=None, help='Path to local application to use, omit to use external apps.')
+@click.option("--app-args", type=str, default='', help='The extra arguments passed to the device.')
+@click.option("--tool-path", type=click.Path(exists=True), default=None, help='Path to java-matter-controller.')
+@click.option("--tool-cluster", type=str, default='pairing', help='The cluster name passed to the java-matter-controller.')
+@click.option("--tool-args", type=str, default='', help='The arguments passed to the java-matter-controller.')
+@click.option("--factoryreset", is_flag=True, help='Remove app configs (/tmp/chip*) before running the tests.')
+def main(app: str, app_args: str, tool_path: str, tool_cluster: str, tool_args: str, factoryreset: bool):
+    logging.info("Execute: {script_command}")
+
+    if factoryreset:
+        # Remove native app config
+        retcode = subprocess.call("rm -rf /tmp/chip*", shell=True)
+        if retcode != 0:
+            raise Exception("Failed to remove /tmp/chip* for factory reset.")
+
+        print("Contents of test directory: %s" % os.getcwd())
+        print(subprocess.check_output(["ls -l"], shell=True).decode('us-ascii'))
+
+        # Remove native app KVS if that was used
+        kvs_match = re.search(r"--KVS (?P<kvs_path>[^ ]+)", app_args)
+        if kvs_match:
+            kvs_path_to_remove = kvs_match.group("kvs_path")
+            retcode = subprocess.call("rm -f %s" % kvs_path_to_remove, shell=True)
+            print("Trying to remove KVS path %s" % kvs_path_to_remove)
+            if retcode != 0:
+                raise Exception("Failed to remove %s for factory reset." % kvs_path_to_remove)
+
+    coloredlogs.install(level='INFO')
+
+    log_queue = queue.Queue()
+    log_cooking_threads = []
+
+    if tool_path:
+        if not os.path.exists(tool_path):
+            if tool_path is None:
+                raise FileNotFoundError(f"{tool_path} not found")
+
+    app_process = None
+    if app:
+        if not os.path.exists(app):
+            if app is None:
+                raise FileNotFoundError(f"{app} not found")
+        app_args = [app] + shlex.split(app_args)
+        logging.info(f"Execute: {app_args}")
+        app_process = subprocess.Popen(
+            app_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0)
+        DumpProgramOutputToQueue(
+            log_cooking_threads, Fore.GREEN + "APP " + Style.RESET_ALL, app_process, log_queue)
+
+    command = ['java', '-Djava.library.path=' + tool_path + '/lib/jni', '-jar', tool_path + '/bin/java-matter-controller']
+
+    if tool_cluster == 'pairing':
+        logging.info("Testing pairing cluster")
+
+        test = CommissioningTest(log_cooking_threads, log_queue, command, tool_args)
+        controller_exit_code = test.RunTest()
+
+        if controller_exit_code != 0:
+            logging.error("Test script exited with error %r" % test_script_exit_code)
+
+    app_exit_code = 0
+    if app_process:
+        logging.warning("Stopping app with SIGINT")
+        app_process.send_signal(signal.SIGINT.value)
+        app_exit_code = app_process.wait()
+
+    # There are some logs not cooked, so we wait until we have processed all logs.
+    # This procedure should be very fast since the related processes are finished.
+    for thread in log_cooking_threads:
+        thread.join()
+
+    if controller_exit_code != 0:
+        sys.exit(controller_exit_code)
+    else:
+        # We expect both app and controller should exit with 0
+        sys.exit(app_exit_code)
+
+
+if __name__ == '__main__':
+    main()