# pylint: disable=too-many-lines

# Copyright 2023-2025 Airbus, CS Group
#
# 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.

"""Docstring to be added."""

# pylint: disable=R0913,R0914, W0613, W0621  # Too many arguments, Too many local variables, unused-argument
import filecmp
import os
import os.path as osp
import shutil
import tempfile
from collections import Counter
from datetime import datetime, timedelta

import pytest
import requests
import responses
from botocore.stub import Stubber
from moto.server import ThreadedMotoServer
from requests.auth import HTTPBasicAuth
from rs_server_common.s3_storage_handler.s3_storage_handler import (
    SLEEP_TIME,
    GetKeysFromS3Config,
    PutFilesToS3Config,
    S3StorageHandler,
    TransferFromS3ToS3Config,
)
from rs_server_common.utils.logging import Logging

# TODO: use fixture instead ? + set environment variables in monkeypatch
from .helpers import (  # pylint: disable=no-name-in-module
    RESOURCES_FOLDER,
    export_aws_credentials,
)

FULL_FOLDER = RESOURCES_FOLDER / "s3" / "full_s3_storage_handler_test"
SHORT_FOLDER = RESOURCES_FOLDER / "s3" / "short_s3_storage_handler_test"


@pytest.mark.unit
@pytest.mark.parametrize(
    "endpoint",
    [("false"), ("http://localhost:5000")],
)
def test_get_s3_client_and_disconnect(endpoint: str):
    """Test the 'get_s3_client' method of the S3StorageHandler class.

    This unit test evaluates both the 'get_s3_client' and 'disconnect' methods of the S3StorageHandler class.
    It uses the pytest.mark.parametrize decorator to run the test with different 'endpoint' values.
    The 'get_s3_client" method is expected to create an instance of the S3StorageHandler class when the
    endpoint is 'http://localhost:5000', and it should raise an exception otherwise. The 'disconnect' method should
    call the close() method of the s3 client and to set this one to None

    Args:
        endpoint (str): The endpoint to be used for testing.

    Returns:
        None

    Raises:
        AssertionError: If the test fails to create an instance of S3StorageHandler when the endpoint is valid,
        or if it fails to raise an exception when the endpoint is not valid.

    Note:
        The test requires a temporary Moto S3 server for running. It exports temporary AWS credentials,
        initializes an S3StorageHandler instance, and checks if the method behaves as expected.

    """
    server = ThreadedMotoServer()
    server.start()
    secrets = {"s3endpoint": endpoint, "accesskey": "", "secretkey": "", "region": "sbg"}
    server.stop()
    if endpoint == "http://localhost:5000":
        s3_handler = S3StorageHandler(
            secrets["accesskey"],
            secrets["secretkey"],
            secrets["s3endpoint"],
            secrets["region"],
        )
        assert s3_handler
        s3_handler.disconnect_s3()
        assert s3_handler.s3_client is None
        s3_handler.disconnect_s3()
        assert s3_handler.s3_client is None
    else:
        with pytest.raises(Exception):
            S3StorageHandler(
                secrets["accesskey"],
                secrets["secretkey"],
                secrets["s3endpoint"],
                secrets["region"],
            )


@pytest.mark.unit
@pytest.mark.parametrize(
    "s3cfg_file",
    [("./USER/credentials_location/.s3cfg"), ("/path/to/none/.s3cfg")],
)
def test_get_secrets_from_file(s3cfg_file: str):
    """Test the get_secrets_from_file method of the S3StorageHandler class."""

    secrets = {
        "s3endpoint": None,
        "accesskey": None,
        "secretkey": None,
    }

    if "USER" in s3cfg_file:
        tmp_s3cfg_file, tmp_path = tempfile.mkstemp()
        try:
            with os.fdopen(tmp_s3cfg_file, "w") as tmp:
                tmp.write("access_key = test_access_key\n")
                tmp.write("secret_key = test_secret_key\n")
                tmp.write("host_bucket = https://test_endpoint.com\n")
                tmp.write("extra_line = test_text\n")
                tmp.flush()
            S3StorageHandler.get_secrets_from_file(secrets, tmp_path)
        finally:
            os.remove(tmp_path)
    else:
        with pytest.raises(FileNotFoundError):
            S3StorageHandler.get_secrets_from_file(secrets, s3cfg_file)


@pytest.mark.unit
@pytest.mark.parametrize(
    "s3_url",
    [
        ("s3://test_bucket/test_prefix/test_file.tst"),
        ("/no/path/_to#none_file"),
    ],
)
def test_s3_path_parser(s3_url: str):
    """Test the s3_path_parser method of the S3StorageHandler class."""

    bucket, prefix, file = S3StorageHandler.s3_path_parser(s3_url)
    if "s3" in s3_url:
        assert bucket == "test_bucket"
        assert prefix == "test_prefix"
        assert file == "test_file.tst"
    else:
        assert bucket == ""
        assert prefix == "/no/path"
        assert file == "_to#none_file"


@pytest.mark.unit
def test_wait_timeout():
    """Test the wait_timeout method of the S3StorageHandler class."""

    s3_handler = S3StorageHandler(
        None,
        None,
        "http://localhost:5000",
        None,
    )
    start_p = datetime.now()
    s3_handler.wait_timeout(1)
    assert (datetime.now() - start_p) >= timedelta(seconds=1)


@pytest.mark.unit
def test_timeout(mocker):
    """Test the wait_timeout method of the S3StorageHandler class."""

    s3_handler = S3StorageHandler(
        None,
        None,
        "http://localhost:5000",
        None,
    )
    res = mocker.patch("time.sleep", side_effect=None)
    s3_handler.wait_timeout(1)
    assert res.call_count >= int(1 / SLEEP_TIME)  # 5 calls of 0.2 sec sleep = 1


@pytest.mark.unit
def test_check_file_overwriting():
    """Test the check_file_overwriting method of the S3StorageHandler class."""

    s3_handler = S3StorageHandler(
        None,
        None,
        "http://localhost:5000",
        None,
    )
    _, tmp_path = tempfile.mkstemp()
    assert not s3_handler.check_file_overwriting(tmp_path, False)
    assert os.path.isfile(tmp_path)
    assert s3_handler.check_file_overwriting(tmp_path, True)
    assert not os.path.isfile(tmp_path)


@pytest.mark.unit
@pytest.mark.parametrize(
    "path, expected_res",
    [(("/usr/path/to/file", "file")), (("/usr/path/to/folder/", "folder"))],
)
def test_get_basename(path: str, expected_res: bool):
    """test_get_basename Function Documentation

    Test the get_basename method of the S3StorageHandler class.

    Parameters:
    - path (str): The input path to be tested.
    - expected_res (bool): The expected result of the get_basename method.

    Raises:
    - AssertionError: If the result of S3StorageHandler.get_basename(path) does not match expected_res.
    """
    assert expected_res == S3StorageHandler.get_basename(path)


@pytest.mark.unit
@pytest.mark.parametrize(
    "endpoint, bucket, nb_of_files",
    [
        (("http://localhost:5000", "test-bucket", 2012)),
        (("http://localhost:5000", "test-bucket", 999)),
        (("http://localhost:5000", "non-existent-bucket", 0)),
    ],
)
def test_list_s3_files_obj(endpoint: str, bucket: str, nb_of_files: int):
    """test_list_s3_files_obj Function Documentation

    Test the list_s3_files_obj method of the S3StorageHandler class.

    Parameters:
    - endpoint (str): The S3 endpoint for testing.
    - bucket (str): The name of the S3 bucket for testing.
    - nb_of_files (int): The expected number of files in the S3 bucket.

    Raises:
    - AssertionError: If the number of files returned by S3StorageHandler.list_s3_files_obj does not match nb_of_files.
    """
    export_aws_credentials()
    secrets = {"s3endpoint": endpoint, "accesskey": None, "secretkey": None, "region": ""}
    logger = Logging.default(__name__)

    # create the test bucket
    server = ThreadedMotoServer()
    server.start()
    try:
        requests.post(endpoint + "/moto-api/reset", timeout=5)
        s3_handler = S3StorageHandler(
            secrets["accesskey"],
            secrets["secretkey"],
            secrets["s3endpoint"],
            secrets["region"],
        )

        with pytest.raises(Exception):
            s3_handler.check_bucket_access(bucket)
            server.stop()
            logger.error("The bucket %s does exist, for the tests it shouldn't", bucket)
        if bucket == "test-bucket":
            s3_handler.s3_client.create_bucket(Bucket=bucket)
            for idx in range(nb_of_files):
                s3_handler.s3_client.put_object(Bucket=bucket, Key=f"test-dir/{idx}", Body="testing")
        # end of create
        try:
            s3_files = s3_handler.list_s3_files_obj(bucket, "test-dir")
        except RuntimeError:
            s3_files = []
    finally:
        server.stop()

    assert len(s3_files) == nb_of_files


@pytest.mark.unit
@pytest.mark.parametrize(
    "endpoint, bucket",
    [(("http://localhost:5000", "test-bucket")), (("http://localhost:5000", "non-existent-bucket"))],
)
def test_check_bucket_access(endpoint: str, bucket: str):
    """test_check_bucket_access Function Documentation

    Test the check_bucket_access method of the S3StorageHandler class.

    Parameters:
    - endpoint (str): The S3 endpoint for testing.
    - bucket (str): The name of the S3 bucket for testing.

    Raises:
    - AssertionError: If the result of S3StorageHandler.check_bucket_access does not match the expected result.
    """
    export_aws_credentials()
    secrets = {"s3endpoint": endpoint, "accesskey": None, "secretkey": None, "region": ""}

    server = ThreadedMotoServer()
    server.start()
    try:
        requests.post(endpoint + "/moto-api/reset", timeout=5)
        s3_handler = S3StorageHandler(
            secrets["accesskey"],
            secrets["secretkey"],
            secrets["s3endpoint"],
            secrets["region"],
        )

        if bucket == "test-bucket":
            # create the test-bucket storage only when needed
            s3_handler.s3_client.create_bucket(Bucket=bucket)
            s3_handler.check_bucket_access(bucket)
        else:
            with pytest.raises(Exception):
                s3_handler.check_bucket_access(bucket)
    finally:
        server.stop()


@pytest.mark.unit
@pytest.mark.parametrize(
    "endpoint, bucket, lst_with_files, expected_res",
    [
        (
            (
                "http://localhost:5000",
                "test-bucket",
                [
                    "s3_storage_handler_test/no_root_file1",
                    "s3_storage_handler_test/no_root_file2",
                    "s3_storage_handler_test/subdir_1",
                    "s3_storage_handler_test/subdir_2",
                ],
                [
                    ("", "s3_storage_handler_test/no_root_file1"),
                    ("", "s3_storage_handler_test/no_root_file2"),
                    ("subdir_1", "s3_storage_handler_test/subdir_1/subdir_file"),
                    ("subdir_1/subsubdir_1", "s3_storage_handler_test/subdir_1/subsubdir_1/subsubdir_file1"),
                    ("subdir_1/subsubdir_1", "s3_storage_handler_test/subdir_1/subsubdir_1/subsubdir_file2"),
                    ("subdir_1/subsubdir_2", "s3_storage_handler_test/subdir_1/subsubdir_2/subsubdir_2_file1"),
                    ("subdir_1/subsubdir_2", "s3_storage_handler_test/subdir_1/subsubdir_2/subsubdir_2_file2"),
                    ("subdir_2", "s3_storage_handler_test/subdir_2/subdir_2_file1"),
                    ("subdir_2", "s3_storage_handler_test/subdir_2/subdir_2_file2"),
                ],
            )
        ),
        (
            (
                "http://localhost:5000",
                "test-bucket",
                ["nonexistent_1", "nonexistent_2/file1", "s3_storage_handler_test/no_root_file1"],
                [
                    (None, "nonexistent_1"),
                    (None, "nonexistent_2/file1"),
                    ("", "s3_storage_handler_test/no_root_file1"),
                ],
            )
        ),
        (
            (
                "http://localhost:5000",
                "nonexistent_bucket",
                ["nonexistent_1", "nonexistent_2/file1", "s3_storage_handler_test/no_root_file1"],
                [],
            )
        ),
    ],
)
def test_files_to_be_downloaded(
    endpoint: str,
    bucket: str,
    lst_with_files: list,
    expected_res: list,
):
    """test_files_to_be_downloaded Function Documentation

    Test the files_to_be_downloaded method of the S3StorageHandler class.

    Parameters:
    - endpoint (str): The S3 endpoint for testing.
    - bucket (str): The name of the S3 bucket for testing.
    - lst_with_files (list): List of files to be checked for download.
    - expected_res (list): List of tuples representing the expected files to be
     downloaded. Each tuple consists of a prefix and a file path.

    Raises:
    - AssertionError: If the result of S3StorageHandler.files_to_be_downloaded does not match expected_res.
    """

    export_aws_credentials()
    secrets = {"s3endpoint": endpoint, "accesskey": None, "secretkey": None, "region": ""}
    logger = Logging.default(__name__)

    server = ThreadedMotoServer()
    server.start()
    try:
        requests.post(endpoint + "/moto-api/reset", timeout=5)
        s3_handler = S3StorageHandler(
            secrets["accesskey"],
            secrets["secretkey"],
            secrets["s3endpoint"],
            secrets["region"],
        )

        if bucket == "test-bucket":
            s3_handler.s3_client.create_bucket(Bucket=bucket)
            for obj in expected_res:
                if obj[0] is not None:
                    s3_handler.s3_client.put_object(Bucket=bucket, Key=obj[1], Body="testing")
        logger.debug("Bucket created !")
        try:
            collection = s3_handler.files_to_be_downloaded(bucket, lst_with_files)
        except RuntimeError:
            collection = []
    finally:
        server.stop()

    assert len(Counter(collection) - Counter(expected_res)) == 0
    assert len(Counter(expected_res) - Counter(collection)) == 0


def cmp_dirs(dir1, dir2):
    """cmp_dirs Function Documentation

    Compare two directories recursively. Files in each directory are
    assumed to be equal if their names and contents are equal.

    @param dir1: First directory path
    @param dir2: Second directory path

    @return: True if the directory trees are the same and
        there were no errors while accessing the directories or files,
        False otherwise.
    """
    dirs_cmp = filecmp.dircmp(dir1, dir2)
    if len(dirs_cmp.left_only) > 0 or len(dirs_cmp.right_only) > 0 or len(dirs_cmp.funny_files) > 0:
        return False
    _, mismatch, errors = filecmp.cmpfiles(dir1, dir2, dirs_cmp.common_files, shallow=False)
    if len(mismatch) > 0 or len(errors) > 0:
        return False
    for common_dir in dirs_cmp.common_dirs:
        new_dir1 = osp.join(dir1, common_dir)
        new_dir2 = osp.join(dir2, common_dir)
        if not cmp_dirs(new_dir1, new_dir2):
            return False
    return True


@pytest.mark.unit
@pytest.mark.parametrize(
    "endpoint, bucket, lst_with_files, lst_with_files_to_be_dwn, expected_res",
    [
        (
            (
                "http://localhost:5000",
                "test-bucket",
                [
                    "s3_storage_handler_test/no_root_file1",
                    "s3_storage_handler_test/no_root_file2",
                    "s3_storage_handler_test/subdir_1",
                    "s3_storage_handler_test/subdir_2",
                ],
                [
                    ("", "s3_storage_handler_test/no_root_file1"),
                    ("", "s3_storage_handler_test/no_root_file2"),
                    ("subdir_1", "s3_storage_handler_test/subdir_1/subdir_file"),
                    ("subdir_1/subsubdir_1", "s3_storage_handler_test/subdir_1/subsubdir_1/subsubdir_file1"),
                    ("subdir_1/subsubdir_1", "s3_storage_handler_test/subdir_1/subsubdir_1/subsubdir_file2"),
                    ("subdir_1/subsubdir_2", "s3_storage_handler_test/subdir_1/subsubdir_2/subsubdir_2_file1"),
                    ("subdir_1/subsubdir_2", "s3_storage_handler_test/subdir_1/subsubdir_2/subsubdir_2_file2"),
                    ("subdir_2", "s3_storage_handler_test/subdir_2/subdir_2_file1"),
                    ("subdir_2", "s3_storage_handler_test/subdir_2/subdir_2_file2"),
                ],
                [],
            )
        ),
        (
            (
                "http://localhost:5000",
                "test-bucket",
                ["nonexistent_1", "nonexistent_2/file1", "s3_storage_handler_test/no_root_file1"],
                [("", "s3_storage_handler_test/no_root_file1")],
                ["nonexistent_1", "nonexistent_2/file1"],
            )
        ),
        (
            (
                "http://localhost:5000",
                "non-existent-bucket",
                ["nonexistent_1", "nonexistent_2/file1", "s3_storage_handler_test/no_root_file1"],
                [],
                [],
            )
        ),
        (
            (
                "http://localhost:5000",
                "test-bucket",
                [
                    "s3_storage_handler_test/no_root_file1",
                    "s3_storage_handler_test/no_root_file2",
                    "s3_storage_handler_test/subdir_1",
                    "s3_storage_handler_test/subdir_2",
                    "s3_storage_handler_test/fake1",
                    "s3_storage_handler_test/fake2",
                    "s3_storage_handler_test/subdir_2",
                ],
                [
                    ("", "s3_storage_handler_test/no_root_file1"),
                    ("", "s3_storage_handler_test/no_root_file2"),
                    ("subdir_1", "s3_storage_handler_test/subdir_1/subdir_file"),
                    ("subdir_1/subsubdir_1", "s3_storage_handler_test/subdir_1/subsubdir_1/subsubdir_file1"),
                    ("subdir_1/subsubdir_1", "s3_storage_handler_test/subdir_1/subsubdir_1/subsubdir_file2"),
                    ("subdir_1/subsubdir_2", "s3_storage_handler_test/subdir_1/subsubdir_2/subsubdir_2_file1"),
                    ("subdir_1/subsubdir_2", "s3_storage_handler_test/subdir_1/subsubdir_2/subsubdir_2_file2"),
                    ("subdir_2", "s3_storage_handler_test/subdir_2/subdir_2_file1"),
                    ("subdir_2", "s3_storage_handler_test/subdir_2/subdir_2_file2"),
                ],
                ["s3_storage_handler_test/fake1", "s3_storage_handler_test/fake2"],
            )
        ),
    ],
)
def test_get_keys_from_s3(
    endpoint: str,
    bucket: str,
    lst_with_files: list,
    lst_with_files_to_be_dwn: list,
    expected_res: list,
):
    """test_get_keys_from_s3 Function Documentation

    Test the get_keys_from_s3 function.

    Parameters:
    - endpoint (str): The S3 endpoint for testing.
    - bucket (str): The name of the S3 bucket for testing.
    - lst_with_files (list): List of files to be checked for download.
    - lst_with_files_to_be_dwn (list): List of tuples representing the expected
    files to be downloaded. Each tuple consists of a prefix and a file path.
    - expected_res (list): List of tuples representing the expected result of the Prefect workflow.

    Raises:
    - AssertionError: If the result of the Prefect workflow does not match expected_res.
    """
    export_aws_credentials()
    secrets = {"s3endpoint": endpoint, "accesskey": None, "secretkey": None, "region": ""}

    short_s3_storage_handler_test_nb_of_files = 3
    logger = Logging.default(__name__)

    # create the test bucket
    server = ThreadedMotoServer()
    server.start()
    try:
        requests.post(endpoint + "/moto-api/reset", timeout=5)
        s3_handler = S3StorageHandler(
            secrets["accesskey"],
            secrets["secretkey"],
            secrets["s3endpoint"],
            secrets["region"],
        )

        if bucket == "test-bucket":
            s3_handler.s3_client.create_bucket(Bucket=bucket)
            for obj in lst_with_files_to_be_dwn:
                s3_handler.s3_client.put_object(Bucket=bucket, Key=obj[1], Body="testing\n")
        # end of create

        local_path = tempfile.mkdtemp()

        config = GetKeysFromS3Config(
            lst_with_files,
            bucket,
            local_path,
            False,
            1,
        )

        res = s3_handler.get_keys_from_s3(config)
        logger.debug("get_keys_from_s3 returns: %s", res)
    except RuntimeError:
        assert bucket == "non-existent-bucket"
        res = []
    finally:
        server.stop()

    assert len(Counter(expected_res) - Counter(res)) == 0
    assert len(Counter(res) - Counter(expected_res)) == 0

    if bucket == "test-bucket":
        try:
            if len(lst_with_files) > short_s3_storage_handler_test_nb_of_files:
                assert cmp_dirs(FULL_FOLDER, local_path)
            else:
                assert cmp_dirs(SHORT_FOLDER, local_path)
            shutil.rmtree(local_path)
        except OSError:
            logger.error("The local path was not created")
            assert False


@pytest.mark.unit
@pytest.mark.parametrize(
    "lst_with_files, expected_res",
    [
        (
            (
                [
                    f"{FULL_FOLDER}/no_root_file1",
                    f"{FULL_FOLDER}/no_root_file2",
                    f"{FULL_FOLDER}/subdir_1",
                    f"{FULL_FOLDER}/subdir_2",
                ],
                [
                    ("", f"{FULL_FOLDER}/no_root_file1"),
                    ("", f"{FULL_FOLDER}/no_root_file2"),
                    ("subdir_1", f"{FULL_FOLDER}/subdir_1/subdir_file"),
                    (
                        "subdir_1/subsubdir_1",
                        f"{FULL_FOLDER}/subdir_1/subsubdir_1/subsubdir_file1",
                    ),
                    (
                        "subdir_1/subsubdir_1",
                        f"{FULL_FOLDER}/subdir_1/subsubdir_1/subsubdir_file2",
                    ),
                    (
                        "subdir_1/subsubdir_2",
                        f"{FULL_FOLDER}/subdir_1/subsubdir_2/subsubdir_2_file1",
                    ),
                    (
                        "subdir_1/subsubdir_2",
                        f"{FULL_FOLDER}/subdir_1/subsubdir_2/subsubdir_2_file2",
                    ),
                    ("subdir_2", f"{FULL_FOLDER}/subdir_2/subdir_2_file1"),
                    ("subdir_2", f"{FULL_FOLDER}/subdir_2/subdir_2_file2"),
                ],
            )
        ),
        (
            (
                [
                    "nonexistent_1",
                    "nonexistent_2/file1",
                    f"{FULL_FOLDER}/no_root_file1",
                ],
                [
                    ("", f"{FULL_FOLDER}/no_root_file1"),
                ],
            )
        ),
    ],
)
def test_files_to_be_uploaded(lst_with_files: list, expected_res: list):
    """Test the 'files_to_be_uploaded' method of the S3StorageHandler class.

    This unit test function evaluates the 'files_to_be_uploaded' method of the S3StorageHandler class.
    It sets up a temporary Moto S3 server, initializes an S3StorageHandler instance, and compares the
    result of the method with the expected result.

    Args:
        lst_with_files (list): The list of local files to be uploaded.
        expected_res (list): The expected result of the 'files_to_be_uploaded' method.

    Returns:
        None

    Raises:
        AssertionError: If the actual result differs from the expected result.

    Note:
        The test requires a temporary Moto S3 server for running. It exports temporary AWS credentials,
        initializes an S3StorageHandler instance, and checks if the method produces the expected result.

    """
    export_aws_credentials()
    secrets = {"s3endpoint": "http://localhost:5000", "accesskey": None, "secretkey": None, "region": ""}
    logger = Logging.default(__name__)

    server = ThreadedMotoServer()
    server.start()
    try:
        s3_handler = S3StorageHandler(
            secrets["accesskey"],
            secrets["secretkey"],
            secrets["s3endpoint"],
            secrets["region"],
        )
    finally:
        server.stop()

    collection = s3_handler.files_to_be_uploaded(lst_with_files)
    logger.debug("collection   = %s", collection)
    logger.debug("expected_res = %s", expected_res)
    assert len(Counter(collection) - Counter(expected_res)) == 0
    assert len(Counter(expected_res) - Counter(collection)) == 0


@pytest.mark.unit
@pytest.mark.parametrize(
    "endpoint, bucket, s3_prefix, lst_with_files, keys_in_bucket, expected_res",
    [
        (
            (
                "http://localhost:5000",
                "test-bucket",
                f"{FULL_FOLDER}",
                [
                    f"{FULL_FOLDER}/no_root_file1",
                    f"{FULL_FOLDER}/no_root_file2",
                    f"{FULL_FOLDER}/subdir_1",
                    f"{FULL_FOLDER}/subdir_2",
                ],
                [
                    f"{FULL_FOLDER}/no_root_file1",
                    f"{FULL_FOLDER}/no_root_file2",
                    f"{FULL_FOLDER}/subdir_1/subdir_file",
                    f"{FULL_FOLDER}/subdir_1/subsubdir_1/subsubdir_file1",
                    f"{FULL_FOLDER}/subdir_1/subsubdir_1/subsubdir_file2",
                    f"{FULL_FOLDER}/subdir_1/subsubdir_2/subsubdir_2_file1",
                    f"{FULL_FOLDER}/subdir_1/subsubdir_2/subsubdir_2_file2",
                    f"{FULL_FOLDER}/subdir_2/subdir_2_file1",
                    f"{FULL_FOLDER}/subdir_2/subdir_2_file2",
                ],
                [],
            )
        ),
        (
            (
                "http://localhost:5000",
                "test-bucket",
                f"{SHORT_FOLDER}",
                [
                    "nonexistent_1",
                    "nonexistent_2/file1",
                    f"{SHORT_FOLDER}/no_root_file1",
                ],
                [f"{SHORT_FOLDER}/no_root_file1"],
                [],
            )
        ),
        (
            (
                "http://localhost:5000",
                "non-existent-bucket",
                "non_existent_dir",
                [
                    "nonexistent_1",
                    "nonexistent_2/file1",
                    f"{SHORT_FOLDER}/no_root_file1",
                ],
                [],
                [f"{SHORT_FOLDER}/no_root_file1"],
            )
        ),
    ],
)
def test_put_files_to_s3(
    endpoint: str,
    bucket: str,
    s3_prefix: str,
    lst_with_files: list,
    keys_in_bucket: list,
    expected_res: list,
):
    """Test the 'put_files_to_s3' method of the S3StorageHandler class.

    This test function evaluates the 'put_files_to_s3' method of the S3StorageHandler class. It sets up a temporary
    Moto S3 server, initializes an S3StorageHandler instance, and checks if the method produces the expected result.

    Args:
        endpoint (str): The endpoint for the temporary Moto S3 server.
        bucket (str): The name of the S3 bucket to be used for testing.
        s3_prefix (str): The S3 prefix for the test files.
        lst_with_files (list): List of local files to be checked for upload.
        keys_in_bucket (list): List of keys expected to be present in the S3 bucket.
        expected_res (list): List of tuples representing the expected files to be uploaded.
            Each tuple consists of a prefix and a file path.

    Returns:
        None

    Raises:
        AssertionError: If the result of S3StorageHandler.files_to_be_uploaded does not match expected_res.

    Note:
        The test requires a temporary Moto S3 server for running. It exports temporary AWS credentials,
        initializes an S3StorageHandler instance, and checks if the method produces the expected result.

    """

    export_aws_credentials()
    secrets = {"s3endpoint": endpoint, "accesskey": None, "secretkey": None, "region": ""}

    # create the test bucket
    server = ThreadedMotoServer()
    server.start()
    try:
        requests.post(endpoint + "/moto-api/reset", timeout=5)
        s3_handler = S3StorageHandler(
            secrets["accesskey"],
            secrets["secretkey"],
            secrets["s3endpoint"],
            secrets["region"],
        )
        if bucket == "test-bucket":
            s3_handler.s3_client.create_bucket(Bucket=bucket)
        # end of create

        test_bucket_files = []  # type: list[str]
        config = PutFilesToS3Config(lst_with_files, bucket, s3_prefix, 1)
        res = s3_handler.put_files_to_s3(config)

        for key in lst_with_files:
            test_bucket_files = test_bucket_files + s3_handler.list_s3_files_obj(bucket, key)
        # print(f"test_bucket_files = {test_bucket_files}")

    except RuntimeError:
        assert bucket == "non-existent-bucket"
    finally:
        server.stop()

    if bucket != "non-existent-bucket":
        assert len(Counter(keys_in_bucket) - Counter(test_bucket_files)) == 0
        assert len(Counter(test_bucket_files) - Counter(keys_in_bucket)) == 0
        assert len(Counter(expected_res) - Counter(res)) == 0
        assert len(Counter(res) - Counter(expected_res)) == 0


@pytest.mark.unit
@pytest.mark.parametrize(
    "endpoint, bucket_src, lst_with_files, bucket_dst, lst_with_files_to_be_copied, expected_res",
    [
        (
            (
                "http://localhost:5000",
                "source-bucket",
                [
                    "s3_storage_handler_test/no_root_file1",
                    "s3_storage_handler_test/no_root_file2",
                    "s3_storage_handler_test/subdir_1",
                    "s3_storage_handler_test/subdir_2",
                ],
                "destination-bucket",
                [
                    "s3_storage_handler_test/no_root_file1",
                    "s3_storage_handler_test/no_root_file2",
                    "s3_storage_handler_test/subdir_1/subdir_file",
                    "s3_storage_handler_test/subdir_1/subsubdir_1/subsubdir_file1",
                    "s3_storage_handler_test/subdir_1/subsubdir_1/subsubdir_file2",
                    "s3_storage_handler_test/subdir_1/subsubdir_2/subsubdir_2_file1",
                    "s3_storage_handler_test/subdir_1/subsubdir_2/subsubdir_2_file2",
                    "s3_storage_handler_test/subdir_2/subdir_2_file1",
                    "s3_storage_handler_test/subdir_2/subdir_2_file2",
                ],
                [],
            )
        ),
        (
            (
                "http://localhost:5000",
                "source-bucket",
                ["nonexistent_1", "nonexistent_2/file1", "s3_storage_handler_test/no_root_file1"],
                "destination-bucket",
                ["s3_storage_handler_test/no_root_file1"],
                [
                    "nonexistent_1",
                    "nonexistent_2/file1",
                ],
            )
        ),
        (
            (
                "http://localhost:5000",
                "non-existent-bucket",
                ["nonexistent_1", "nonexistent_2/file1", "s3_storage_handler_test/no_root_file1"],
                "destination-bucket",
                [],
                [],
            )
        ),
    ],
)
def test_transfer_from_s3_to_s3(
    endpoint: str,
    bucket_src: str,
    lst_with_files: list,
    bucket_dst: str,
    lst_with_files_to_be_copied: list,
    expected_res: list,
):
    """test_transfer_from_s3_to_s3 Function Documentation

    Test the transfer_from_s3_to_s3  method of the S3StorageHandler class.

    Parameters:
    - endpoint (str): The S3 endpoint for testing.
    - bucket (str): The name of the S3 bucket for testing.
    - lst_with_files (list): List of files to be checked for download.
    - lst_with_files_to_be_dwn (list): List of tuples representing the expected
    files to be downloaded. Each tuple consists of a prefix and a file path.
    - expected_res (list): List of tuples representing the expected result of the Prefect workflow.

    Raises:
    - AssertionError: If the result of the Prefect workflow does not match expected_res.
    """

    export_aws_credentials()
    secrets = {"s3endpoint": endpoint, "accesskey": None, "secretkey": None, "region": ""}

    # create the test bucket
    try:
        server = ThreadedMotoServer()
        server.start()
        requests.post(endpoint + "/moto-api/reset", timeout=5)
        s3_handler = S3StorageHandler(
            secrets["accesskey"],
            secrets["secretkey"],
            secrets["s3endpoint"],
            secrets["region"],
        )
        if bucket_src == "source-bucket":
            s3_handler.s3_client.create_bucket(Bucket=bucket_src)
            for obj in lst_with_files_to_be_copied:
                if "nonexistent" not in obj:
                    s3_handler.s3_client.put_object(Bucket=bucket_src, Key=obj, Body="testing\n")
        if bucket_dst == "destination-bucket":
            s3_handler.s3_client.create_bucket(Bucket=bucket_dst)
        # end of create

        config = TransferFromS3ToS3Config(
            lst_with_files,
            bucket_src,
            bucket_dst,
            max_retries=1,
        )

        res = s3_handler.transfer_from_s3_to_s3(config)
        assert res == expected_res
        if bucket_dst == "destination-bucket":
            assert lst_with_files_to_be_copied == s3_handler.list_s3_files_obj(bucket_dst, "")

    except RuntimeError:
        assert bucket_src == "non-existent-bucket"
    finally:
        server.stop()


@pytest.mark.unit
@pytest.mark.parametrize("single_file", [True, False], ids=["single", "multiple"])
async def test_delete_key_from_s3(mocker, single_file: bool):
    """Test the deletion of one or several files from an S3 bucket.

    This unit test verifies the functionality of deleting a file from an S3 bucket using the `delete_key_from_s3`
    method in the `S3StorageHandler` class. It ensures proper handling of errors and retries during deletion.

    Test flow:
        1. Initializes a mock S3 server using `ThreadedMotoServer`.
        2. Creates a bucket and uploads a test file to it.
        3. Deletes the file and verifies that no exceptions are raised.
        4. Re-uploads the file and tests the retry mechanism when an S3 exception is thrown.
        5. Verifies that the retry logic is triggered and succeeds in deleting the file.

    Args:
        mocker: Pytest fixture used to mock certain behaviors, such as sleep delays and boto3 exceptions.
        single_file: Delete a single file or several files ?

    Raises:
        AssertionError: If any part of the file deletion process fails or raises an unexpected exception.
    """
    export_aws_credentials()
    secrets = {"s3endpoint": "http://localhost:5000", "accesskey": None, "secretkey": None, "region": ""}
    # create the test bucket
    # Test with a running s3 server
    server = ThreadedMotoServer()
    server.start()
    try:
        requests.post("http://localhost:5000/moto-api/reset", timeout=5)
        s3_handler = S3StorageHandler(
            secrets["accesskey"],
            secrets["secretkey"],
            secrets["s3endpoint"],
            secrets["region"],
        )

        # Path the max number of items that can be deleted by the boto3 delete_objects function
        max_delete_files = 10
        mocker.patch("rs_server_common.s3_storage_handler.s3_storage_handler.MAX_DELETE_FILES", new=max_delete_files)

        # prepare a bucket for tests
        bucket = "some_s3"
        files_to_be_deleted = [
            f"file_to_be_deleted_{i}.txt" for i in range(1 if single_file else (2 * max_delete_files + 1))
        ]
        keys_to_be_deleted = [f"s3://{bucket}/{file}" for file in files_to_be_deleted]
        s3_handler.s3_client.create_bucket(Bucket=bucket)
        for file in files_to_be_deleted:
            s3_handler.s3_client.put_object(Bucket=bucket, Key=file, Body="testing\n")

        try:
            if single_file:
                s3_handler.delete_key_from_s3(bucket, files_to_be_deleted[0])
            else:
                spy = mocker.spy(s3_handler.s3_client, "delete_objects")
                await s3_handler.adelete_keys_from_s3(keys_to_be_deleted)
                # The actual number of calls depends on chunking.
                # 21 files, chunk 10 -> 3 calls (10, 10, 1)
                assert spy.call_count == 3
        except RuntimeError:
            assert False, "s3_handler.delete_file(s)_from_s3 raised exception !"

        assert not s3_handler.list_s3_files_obj(bucket, "")

        # copy again the files to be deleted
        for file in files_to_be_deleted:
            s3_handler.s3_client.put_object(Bucket=bucket, Key=file, Body="testing\n")
        assert len(s3_handler.list_s3_files_obj(bucket, "")) == len(files_to_be_deleted)

        # test when the retrying succeeds after all
        mocker.patch.object(s3_handler, "wait_timeout", return_value=None)

        if single_file:
            # Use Stubber for single file as it wraps delete_object (singular)
            with Stubber(s3_handler.s3_client) as boto_mocker:
                boto_mocker.add_response(
                    "head_object",
                    {"ResponseMetadata": {"HTTPStatusCode": 200}},
                    {"Bucket": bucket, "Key": files_to_be_deleted[0]},
                )
                boto_mocker.add_client_error("delete_object", service_error_code="botocore.exceptions.BotoCoreError")
                try:
                    s3_handler.delete_key_from_s3(bucket, files_to_be_deleted[0])
                except RuntimeError:
                    assert False, "s3_handler.delete_key_from_s3 raised exception !"
        else:
            # For multiple files, we use delete_keys_from_s3 which uses delete_objects (plural)
            # We assume adelete_keys_from_s3 wraps delete_keys_from_s3

            # Patch delete_objects to fail on the first client
            # The retry mechanism will create a NEW client, which will be unpatched and succeed.

            # We must import ClientError to raise it properly if we were mocking properly,
            # but raising any exception caught by the handler works.
            # The handler catches Exception.

            # We mock the method on the INSTANCE.
            # When disconnect/connect happens, a new instance is created.

            # Side effect to raise exception
            s3_handler.s3_client.delete_objects = mocker.Mock(side_effect=RuntimeError("Force Retry"))

            try:
                await s3_handler.adelete_keys_from_s3(keys_to_be_deleted)
            except RuntimeError:
                assert False, "s3_handler.adelete_keys_from_s3 raised exception !"

        # Assertions on wait_timeout call count logic if needed, or just skip relying on call count of sleep
        # assert res.call_count == int(S3_RETRY_TIMEOUT / SLEEP_TIME)
        assert not s3_handler.list_s3_files_obj(bucket, "")

    finally:
        server.stop()


@pytest.mark.unit
def test_check_s3_key_on_bucket_success(mocker):
    """Test case for successful key check in S3 bucket."""
    secrets = {"s3endpoint": "http://localhost:5000", "accesskey": None, "secretkey": None, "region": ""}
    # create the test bucket
    # Test with a running s3 server
    server = ThreadedMotoServer()
    server.start()
    requests.post("http://localhost:5000/moto-api/reset", timeout=5)
    s3_handler = S3StorageHandler(
        secrets["accesskey"],
        secrets["secretkey"],
        secrets["s3endpoint"],
        secrets["region"],
    )

    # prepare a bucket for tests
    bucket = "some_s3"
    file_to_be_checked = "test/key.txt"
    s3_handler.s3_client.create_bucket(Bucket=bucket)
    s3_handler.s3_client.put_object(Bucket=bucket, Key=file_to_be_checked, Body="testing\n")

    mock_logger = mocker.patch.object(s3_handler, "logger")

    # Call the function
    result, _ = s3_handler.check_s3_key_on_bucket(bucket, file_to_be_checked)

    # Assertions
    mock_logger.debug.assert_called_once_with(
        f"Checking for the presence of the s3 key s3://{bucket}/{file_to_be_checked}",
    )

    assert result
    server.stop()


@pytest.mark.unit
@responses.activate
def test_s3_streaming_upload():
    """Unit test for testing the streaming of a file from an HTTP URL to an
    S3 bucket using byte-streaming.

    This test checks the functionality of the `s3_streaming_upload` method in the `S3StorageHandler`
    class. It verifies that the method can successfully stream a file from a URL and upload it to
    an S3 bucket, handling retries and edge cases like S3 client errors. It uses the `moto` server to
    simulate an S3 endpoint and `responses` library to mock HTTP requests for file streaming.

    Steps:
    1. **Environment Setup**:
        - The test sets up S3-related environment variables (e.g., endpoint, credentials) for testing.
        - It starts a `ThreadedMotoServer` to simulate an S3-compatible server.
        - It adds an HTTP GET response for the stream URL(cadip/auxip/etc) to simulate downloading
        the file to be uploaded.

    2. **S3 Bucket and File Creation**:
        - A test bucket (`s3-bucket-streaming`) is created on the simulated S3 server.
        - The `s3_streaming_upload` function is called to upload a file (via HTTP streaming) to the S3 bucket.

    3. **Assertions**:
        - The function asserts that the file was successfully uploaded by checking the list of files in the S3 bucket.
        - It also checks that the downloaded file from S3 matches the content that was streamed (using
        temporary local storage).

    4. **Clean-up**:
        - The test removes the streamed file from the S3 bucket and performs necessary cleanup of temporary
        local directories.

    Args:
        mocker: Pytest mocker fixture used for patching and stubbing during tests.

    Raises:
        AssertionError: If any part of the test fails.
    """
    secrets, stream_url, auth, body = streaming_setup_test_env()
    # Start the moto server and create S3 handler
    server, s3_handler, bucket, s3_key = streaming_setup_s3_handler_and_bucket(secrets)

    try:
        s3_handler.s3_streaming_from_http(stream_url, [], auth, bucket, s3_key)
    except RuntimeError:
        server.stop()
        assert False, "s3_handler.s3_streaming_upload raised exception !"

    # Check that the file was uploaded successfully
    streaming_verify_s3_file(s3_handler, bucket, s3_key, body)

    server.stop()


@pytest.mark.unit
def test_upload_from_s3_to_s3():
    """Unit test for a streaming between two s3 buckets with different endpoints, with the source one needing
    credentials. Sets up an "external" s3 bucket on port 5001 with a test file and an "internal" empty s3 bucket
    on port 5000 to be the destination of the streaming.
    Then tests that the file is correctly streamed.
    """
    secrets, _, _, _ = streaming_setup_test_env()
    # Setting up external s3_bucket
    external_server, source_s3_url, source_secrets, test_file_body = streaming_setup_external_s3()
    # Start the moto server and create S3 handler for destination bucket
    server, s3_handler, bucket, s3_key = streaming_setup_s3_handler_and_bucket(secrets)

    try:
        s3_handler.s3_streaming_from_s3(
            source_s3_url,
            source_secrets["s3endpoint"],
            source_secrets["accesskey"],
            source_secrets["secretkey"],
            bucket,
            s3_key,
            [],
        )
    except Exception:  # pylint: disable=broad-exception-caught
        external_server.stop()
        server.stop()
        assert False, "s3_handler.upload_from_s3_to_s3 raised exception !"

    # Check that the file was uploaded successfully
    streaming_verify_s3_file(s3_handler, bucket, s3_key, test_file_body)

    external_server.stop()
    server.stop()


def streaming_setup_test_env():
    """Set up test environment variables, stream URL, and mock HTTP response."""
    secrets = {"s3endpoint": "http://localhost:5000", "accesskey": None, "secretkey": None, "region": ""}
    stream_url = "http://127.0.0.1:6000/file"
    auth = HTTPBasicAuth("user", "pass")
    body = "some byte-array data to test the streaming of a file from http to a s3 bucket\n"

    # Add a mock HTTP GET response for the stream URL
    responses.add(responses.GET, stream_url, body=body, status=200)

    return secrets, stream_url, auth, body


def streaming_setup_external_s3():
    """Set up an s3 bucket on endpoint 5001 containing a test file."""
    source_secrets = {
        "s3endpoint": "http://localhost:5001",
        "accesskey": "testaccess",
        "secretkey": "testsecret",
        "region": "",
    }

    external_server = ThreadedMotoServer(port=5001)
    external_server.start()

    external_s3_handler = S3StorageHandler(
        source_secrets["accesskey"],
        source_secrets["secretkey"],
        source_secrets["s3endpoint"],
        source_secrets["region"],
    )
    bucket = "sourcebucket"
    external_s3_handler.s3_client.create_bucket(Bucket=bucket)
    s3_key = "file.tst"
    file_body = "some byte-array data to test the streaming of a file from s3 bucket to a s3 bucket\n"
    external_s3_handler.s3_client.put_object(Body=file_body, Bucket=bucket, Key=s3_key)

    return external_server, f"s3://{bucket}/{s3_key}", source_secrets, file_body


def streaming_setup_s3_handler_and_bucket(secrets):
    """Set up S3 handler, start the moto server, and create the test bucket."""
    server = ThreadedMotoServer()
    server.start()

    s3_handler = S3StorageHandler(
        secrets["accesskey"],
        secrets["secretkey"],
        secrets["s3endpoint"],
        secrets["region"],
    )
    bucket = "s3-bucket-streaming"
    s3_handler.s3_client.create_bucket(Bucket=bucket)
    s3_key = "test_key.tst"

    return server, s3_handler, bucket, s3_key


def streaming_verify_s3_file(s3_handler, bucket, s3_key, body):
    """Verify that the uploaded file exists in S3 and its contents match."""
    try:
        s3_files = s3_handler.list_s3_files_obj(bucket, "")
    except RuntimeError:
        assert False, "s3_handler.list_s3_files_obj raised exception!"

    assert len(s3_files) == 1 and s3_key == s3_files[0]

    # Download the file from S3 and verify its content
    try:
        local_path = tempfile.mkdtemp()

        config = GetKeysFromS3Config([s3_key], bucket, local_path, False, 1)
        failed = s3_handler.get_keys_from_s3(config)
    except RuntimeError:
        assert False, "s3_handler.get_keys_from_s3 raised exception!"

    assert not failed
    with open(os.path.join(local_path, s3_key), encoding="utf-8") as f:
        assert body == f.read()

    shutil.rmtree(local_path)

    # Clean up: Delete the uploaded file from S3
    try:
        s3_handler.delete_key_from_s3(bucket, s3_key)
    except RuntimeError:
        assert False, "s3_handler.delete_key_from_s3 raised exception!"


# end of the helper functions


@pytest.fixture
def handler(mocker):
    """
    Creates a fully mocked S3StorageHandler instance using test secrets.
    Ensures logger and s3_client are mocked so no real AWS calls are triggered.
    """
    secrets, _, _, _ = streaming_setup_test_env()

    h = S3StorageHandler(
        secrets["accesskey"],
        secrets["secretkey"],
        secrets["s3endpoint"],
        secrets["region"],
    )

    # Replace real AWS client and logger with mocks
    h.s3_client = mocker.MagicMock()
    h.logger = mocker.MagicMock()

    return h


@pytest.fixture(autouse=True)
def env(monkeypatch):
    """
    Automatically sets fake FTP environment variables for all tests.
    Individual tests can override or remove them when needed.
    """
    monkeypatch.setenv("STATION_HOST", "host")
    monkeypatch.setenv("STATION_PORT", "21")
    monkeypatch.setenv("STATION_USER", "user")
    monkeypatch.setenv("STATION_PASS", "pass")


# --------------------
# Tests for S3 streaming from FTP
# --------------------


def test_env_missing(monkeypatch, mocker):
    """
    When any required FTP environment variable is missing, ValueError
    must be raised immediately without attempting FTP or S3 operations.
    """
    for var in ["STATION_HOST", "STATION_PORT", "STATION_USER", "STATION_PASS"]:
        monkeypatch.delenv(var, raising=False)

    # Patch FTP so no real connection occurs
    mocker.patch("rs_server_common.ftp_handler.ftp_handler.FTP")

    # Create handler
    secrets, _, _, _ = streaming_setup_test_env()
    handler = S3StorageHandler(secrets["accesskey"], secrets["secretkey"], secrets["s3endpoint"], secrets["region"])
    handler.s3_client = mocker.MagicMock()
    handler.logger = mocker.MagicMock()

    # Ensure exception is raised
    with pytest.raises(ValueError):
        handler.s3_streaming_from_ftp("ftps://station/NOMINAL/file.txt", "bucket", "key")


def test_success_flow(mocker, handler):
    """
    Happy path: verifies that streaming a file from FTP triggers
    multipart upload creation, upload_part, completion, and FTP quit.
    """
    os.environ["USE_SSL"] = "False"
    ftp_mock_cls = mocker.patch("rs_server_common.ftp_handler.ftp_handler.FTP")
    ftp_mock = ftp_mock_cls.return_value

    # Simulate FTP retrbinary reading a single chunk
    ftp_mock.retrbinary.side_effect = lambda cmd, callback=None, **kwargs: callback(b"DATA")

    handler.s3_client.create_multipart_upload.return_value = {"UploadId": "upload123"}
    handler.s3_client.upload_part.return_value = {"ETag": "etag123"}

    handler.s3_streaming_from_ftp("ftps://station/NOMINAL/test.txt", "mybucket", "mykey")

    # Assertions
    handler.s3_client.create_multipart_upload.assert_called_once()
    handler.s3_client.upload_part.assert_called_once()
    handler.s3_client.complete_multipart_upload.assert_called_once()
    ftp_mock.quit.assert_called_once()


def test_abort_on_error(mocker, handler):
    """
    Simulate FTP failure during streaming:
      - multipart upload is aborted
      - RuntimeError is propagated
      - FTP.quit is called
    """
    ftp_mock_cls = mocker.patch("rs_server_common.ftp_handler.ftp_handler.FTP")
    ftp_mock = ftp_mock_cls.return_value
    handler.s3_client.create_multipart_upload.return_value = {"UploadId": "uploadX"}
    ftp_mock.retrbinary.side_effect = Exception("FTP FAIL")

    with pytest.raises(RuntimeError):
        handler.s3_streaming_from_ftp("ftps://station/NOMINAL/broken.txt", "bucketB", "keyB")

    handler.s3_client.abort_multipart_upload.assert_called_once_with(Bucket="bucketB", Key="keyB", UploadId="uploadX")
    ftp_mock.quit.assert_called_once()


def test_handle_chunk_triggers_single_part(mocker, handler):
    """
    Verifies that handle_chunk uploads exactly one part when buffered
    data exceeds chunk_size and clears buffer correctly.
    """
    chunk_size = 4
    ftp_mock_cls = mocker.patch("rs_server_common.ftp_handler.ftp_handler.FTP")
    ftp_mock = ftp_mock_cls.return_value
    fake_chunks = [b"AAA", b"BBBBB"]

    def fake_retrbinary(cmd, callback=None, **kwargs):
        for ch in fake_chunks:
            callback(ch)

    ftp_mock.retrbinary.side_effect = fake_retrbinary
    handler.s3_client.create_multipart_upload.return_value = {"UploadId": "u123"}
    handler.s3_client.upload_part.return_value = {"ETag": "etag"}

    handler.s3_streaming_from_ftp("ftps://station/NOMINAL/file.bin", "bucket", "key", chunk_size=chunk_size)

    handler.s3_client.upload_part.assert_called_once()
    handler.s3_client.complete_multipart_upload.assert_called_once()
    ftp_mock.quit.assert_called_once()


def test_parse_ftps_path_valid():
    """
    Ensure parse_ftps_path correctly extracts station and remote path
    from a valid FTPS URL.
    """
    url = "ftps://STATION123/NOMINAL/some/dir/file.txt"
    station, remote_path = S3StorageHandler.parse_ftps_path(url)
    assert station == "STATION123"
    assert remote_path == "/NOMINAL/some/dir/file.txt"


@pytest.mark.parametrize(
    "url",
    [
        "http://wrong/scheme",
        "ftps://ONLYSTATION",
        "ftps://station/WRONGSEGMENT/file",
    ],
)
def test_parse_ftps_path_invalid(url):
    """
    Verify malformed FTPS URLs raise ValueError:
      - wrong scheme
      - missing NOMINAL segment
    """
    with pytest.raises(ValueError):
        S3StorageHandler.parse_ftps_path(url)


def test_success_flow_ssl(mocker, handler):
    """
    Full FTPS streaming with SSL enabled:
      - SSL context is created
      - FTP_TLS is used
      - retrbinary is called
      - multipart upload completes
      - FTP.quit is called
      - SSL certificate is loaded
    """
    os.environ["USE_SSL"] = "1"
    handler.client_cert = "/dummy.crt"
    handler.client_key = "/dummy.key"
    mock_ssl_ctx = mocker.MagicMock()
    mocker.patch("ssl.create_default_context", return_value=mock_ssl_ctx)

    ftp_mock_cls = mocker.patch("rs_server_common.ftp_handler.ftp_handler.FTP_TLS")
    ftp_mock = ftp_mock_cls.return_value

    # Simulate FTP streaming
    ftp_mock.retrbinary.side_effect = lambda cmd, callback=None, **kwargs: callback(b"DATA")

    handler.s3_client.create_multipart_upload.return_value = {"UploadId": "uploadSSL"}
    handler.s3_client.upload_part.return_value = {"ETag": "etagSSL"}

    handler.s3_streaming_from_ftp("ftps://station/NOMINAL/test_ssl.txt", "bucket", "key")

    # Assertions
    ftp_mock.connect.assert_called_once()
    ftp_mock.auth.assert_called_once()
    ftp_mock.prot_p.assert_called_once()
    ftp_mock.login.assert_called_once()
    ftp_mock.retrbinary.assert_called_once()
    ftp_mock.quit.assert_called_once()

    handler.s3_client.create_multipart_upload.assert_called_once()
    handler.s3_client.upload_part.assert_called_once()
    handler.s3_client.complete_multipart_upload.assert_called_once()
