# Copyright 2022 The Pigweed 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
#
#     https://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.
"""A variety of transfer tests that validate backwards compatibility.

Usage:

   bazel run pw_transfer/integration_test:legacy_binaries_test

Command-line arguments must be provided after a double-dash:

   bazel run pw_transfer/integration_test:legacy_binaries_test -- \
       --server-port 3304

Which tests to run can be specified as command-line arguments:

  bazel run pw_transfer/integration_test:legacy_binaries_test -- \
      LegacyClientTransferIntegrationTests.test_small_client_write_0_cpp

"""

import itertools
from parameterized import parameterized
import random

from pigweed.pw_transfer.integration_test import config_pb2
from pigweed.pw_transfer.integration_test import test_fixture
from test_fixture import TransferIntegrationTestHarness
from rules_python.python.runfiles import runfiles

# Each set of transfer tests uses a different client/server port pair to
# allow tests to be run in parallel.
_SERVER_PORT = 3314
_CLIENT_PORT = 3315


# NOTE: These backwards compatibility tests DO NOT include tests that verify
# expected error cases (e.g. timeouts, unknown resource ID) because legacy
# integration test clients did not support the ability to check those.
# Additionally, there are deliberately NOT back-to-back read/write transfers
# because of known issues with transfer cleanup in the legacy transfer protocol.
class LegacyTransferIntegrationTest(test_fixture.TransferIntegrationTest):
    """This base class defines the tests to run, but isn't run directly."""

    # Explicitly use UNKNOWN_VERSION (the default value of
    # TransferAction.protocol_version), as that will cause protocol_version to
    # be omitted from the generated text proto, which is critical for the legacy
    # client that doesn't support transfer version specifications (and will
    # cause a proto parse error).
    PROTOCOL_VERSION = config_pb2.TransferAction.ProtocolVersion.UNKNOWN_VERSION
    LEGACY_SERVER = False
    LEGACY_CLIENT = False

    def default_config(self) -> test_fixture.TransferConfig:
        # The legacy binaries aren't aware of the max_lifetime_retries field,
        # which was added more recently. Clear it so it isn't encoded into the
        # serialized message.
        config = super().default_config()
        config.client.max_lifetime_retries = 0
        return config

    @parameterized.expand(
        [
            ("cpp"),
            ("java"),
            ("python"),
        ]
    )
    def test_single_byte_client_write(self, client_type):
        if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
            self.skipTest("No legacy binary in use, skipping")

        if not self.LEGACY_SERVER and (
            client_type == "java" or client_type == "python"
        ):
            self.skipTest("Java and Python legacy clients not yet set up")

        payload = b"?"
        config = self.default_config()
        resource_id = 5
        self.do_single_write(
            "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
        )

    @parameterized.expand(
        [
            ("cpp"),
            ("java"),
            ("python"),
        ]
    )
    def test_small_client_write(self, client_type):
        if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
            self.skipTest("No legacy binary in use, skipping")

        if not self.LEGACY_SERVER and (
            client_type == "java" or client_type == "python"
        ):
            self.skipTest("Java and Python legacy clients not yet set up")

        payload = b"some data"
        config = self.default_config()
        resource_id = 5
        self.do_single_write(
            "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
        )

    @parameterized.expand(
        [
            ("cpp"),
            ("java"),
            ("python"),
        ]
    )
    def test_medium_hdlc_escape_client_write(self, client_type):
        if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
            self.skipTest("No legacy binary in use, skipping")

        if not self.LEGACY_SERVER and (
            client_type == "java" or client_type == "python"
        ):
            self.skipTest("Java and Python legacy clients not yet set up")

        payload = b"~" * 8731
        config = self.default_config()
        resource_id = 12345678
        self.do_single_write(
            "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
        )

    @parameterized.expand(
        [
            ("cpp"),
            ("java"),
            ("python"),
        ]
    )
    def test_medium_random_data_client_write(self, client_type):
        if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
            self.skipTest("No legacy binary in use, skipping")

        if not self.LEGACY_SERVER and (
            client_type == "java" or client_type == "python"
        ):
            self.skipTest("Java and Python legacy clients not yet set up")

        rng = random.Random(1533659510898)
        payload = rng.randbytes(13713)
        config = self.default_config()
        resource_id = 12345678
        self.do_single_write(
            "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
        )

    @parameterized.expand(
        [
            ("cpp"),
            ("java"),
            ("python"),
        ]
    )
    def test_single_byte_client_read(self, client_type):
        if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
            self.skipTest("No legacy binary in use, skipping")

        if not self.LEGACY_SERVER and (
            client_type == "java" or client_type == "python"
        ):
            self.skipTest("Java and Python legacy clients not yet set up")

        payload = b"?"
        config = self.default_config()
        resource_id = 5
        self.do_single_read(
            "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
        )

    @parameterized.expand(
        [
            ("cpp"),
            ("java"),
            ("python"),
        ]
    )
    def test_small_client_read(self, client_type):
        if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
            self.skipTest("No legacy binary in use, skipping")

        if not self.LEGACY_SERVER and (
            client_type == "java" or client_type == "python"
        ):
            self.skipTest("Java and Python legacy clients not yet set up")

        payload = b"some data"
        config = self.default_config()
        resource_id = 5
        self.do_single_read(
            "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
        )

    @parameterized.expand(
        [
            ("cpp"),
            ("java"),
            ("python"),
        ]
    )
    def test_medium_hdlc_escape_client_read(self, client_type):
        if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
            self.skipTest("No legacy binary in use, skipping")

        if self.LEGACY_SERVER:
            self.skipTest("Legacy server has HDLC buffer sizing issues")

        payload = b"~" * 8731
        config = self.default_config()
        resource_id = 5
        self.do_single_read(
            "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
        )

    @parameterized.expand(
        [
            ("cpp"),
            ("java"),
            ("python"),
        ]
    )
    def test_medium_random_data_client_read(self, client_type):
        if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
            self.skipTest("No legacy binary in use, skipping")

        if self.LEGACY_SERVER:
            self.skipTest("Legacy server has HDLC buffer sizing issues")

        rng = random.Random(1533659510898)
        payload = rng.randbytes(13713)
        config = self.default_config()
        resource_id = 5
        self.do_single_read(
            "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
        )


class LegacyClientTransferIntegrationTests(LegacyTransferIntegrationTest):
    r = runfiles.Create()
    client_binary = r.Rlocation(
        "pw_transfer_test_binaries/cpp_client_528098d5",
        "pw_transfer_test_binaries",
    )
    HARNESS_CONFIG = TransferIntegrationTestHarness.Config(
        cpp_client_binary=client_binary,
        server_port=_SERVER_PORT,
        client_port=_CLIENT_PORT,
    )
    LEGACY_CLIENT = True


class LegacyServerTransferIntegrationTests(LegacyTransferIntegrationTest):
    r = runfiles.Create()
    server_binary = r.Rlocation(
        "pw_transfer_test_binaries/server_528098d5", "pw_transfer_test_binaries"
    )
    HARNESS_CONFIG = TransferIntegrationTestHarness.Config(
        server_binary=server_binary,
        server_port=_SERVER_PORT,
        client_port=_CLIENT_PORT,
    )
    LEGACY_SERVER = True


if __name__ == '__main__':
    test_fixture.run_tests_for(LegacyClientTransferIntegrationTests)
    test_fixture.run_tests_for(LegacyServerTransferIntegrationTests)
