# Copyright (C) 2009-2023, Stefan Schwarzer <sschwarzer@sschwarzer.net>
# and ftputil contributors (see `doc/contributors.txt`)
# See the file LICENSE for licensing terms.

import os
import socket
import subprocess

import pytest

import ftputil

import test


def email_address():
    """
    Return the email address used to identify the client to an FTP server.

    If the hostname is "warpy", use my (Stefan's) email address, else try to
    use the content of the `$EMAIL` environment variable. If that doesn't
    exist, use a dummy address.
    """
    hostname = socket.gethostname()
    if hostname == "warpy":
        email = "sschwarzer@sschwarzer.net"
    else:
        dummy_address = "anonymous@example.com"
        email = os.environ.get("EMAIL", dummy_address)
        if not email:
            # Environment variable exists but content is an empty string
            email = dummy_address
    return email


EMAIL = email_address()


def ftp_client_listing(server, directory):
    """
    Log into the FTP server `server` using the command line client, then change
    to the `directory` and retrieve a listing with "dir".

    Return the list of items found as an `os.listdir` would return it.
    """
    # The `-n` option prevents an auto-login.
    ftp_popen = subprocess.Popen(
        ["ftp", "-n", server],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        universal_newlines=True,
    )
    commands = ["user anonymous {}".format(EMAIL), "dir", "bye"]
    if directory:
        # Change to this directory before calling "dir".
        commands.insert(1, "cd {}".format(directory))
    input_ = "\n".join(commands)
    stdout, unused_stderr = ftp_popen.communicate(input_)
    # Collect the directory/file names from the listing's text
    names = []
    for line in stdout.strip().split("\n"):
        if line.startswith("total ") or line.startswith("Trying "):
            continue
        parts = line.split()
        if parts[-2] == "->":
            # Most likely a link
            name = parts[-3]
        else:
            name = parts[-1]
        names.append(name)
    # Remove entries for current and parent directory since they aren't
    # included in the result of `FTPHost.listdir` either.
    names = [name for name in names if name not in (".", "..")]
    return names


class TestPublicServers:
    """
    Get directory listings from various public FTP servers with a command line
    client and ftputil and compare both.

    An important aspect is to test different "spellings" of the same directory.
    For example, to list the root directory which is usually set after login,
    use "" (nothing), ".", "/", "/.", "./.", "././", "..", "../.", "../.." etc.

    The command line client `ftp` has to be in the path.
    """

    # Implementation note:
    #
    # I (Stefan) implement the code so it works with Ubuntu's client. Other
    # clients may work or not. If you have problems testing some other client,
    # please send me a (small) patch. Keep in mind that I don't plan supporting
    # as many FTP obscure commandline clients as servers. ;-)

    # List of pairs with server name and a directory "guaranteed to exist"
    # under the login directory which is assumed to be the root directory.
    servers = [
        # Posix format
        ("ftp.de.debian.org", "debian"),
        ("ftp.heise.de", "pub"),
        ("ftp.tu-chemnitz.de", "pub"),
        ("ftp.uni-erlangen.de", "pub"),
        # DOS/Microsoft format
        # Do you know any FTP servers that use Microsoft format?
        # `ftp.microsoft.com` doesn't seem to be reachable anymore.
    ]

    # This data structure contains the initial directories "." and "DIR" (which
    # will be replaced by a valid directory name for each server). The list
    # after the initial directory contains paths that will be queried after
    # changing into the initial directory. All items in these lists are
    # actually supposed to yield the same directory contents.
    paths_table = [
        (
            ".",
            [
                ".",
                "/",
                "/.",
                "./.",
                "././",
                "..",
                "../.",
                "../..",
                "DIR/..",
                "/DIR/../.",
                "/DIR/../..",
            ],
        ),
        ("DIR", [".", "/DIR", "/DIR/", "../DIR", "../../DIR"]),
    ]

    def inner_test_server(self, server, initial_directory, paths):
        """
        Test one server for one initial directory.

        Connect to the server `server`; if the string argument
        `initial_directory` has a true value, change to this directory. Then
        iterate over all strings in the sequence `paths`, comparing the results
        of a listdir call with the listing from the command line client.
        """
        canonical_names = ftp_client_listing(server, initial_directory)
        host = ftputil.FTPHost(server, "anonymous", EMAIL)
        try:
            host.chdir(initial_directory)
            for path in paths:
                path = path.replace("DIR", initial_directory)
                # Make sure that we don't recycle directory entries, i. e.
                # really repeatedly retrieve the directory contents (shouldn't
                # happen anyway with the current implementation).
                host.stat_cache.clear()
                names = host.listdir(path)
                # Filter out "hidden" names since the FTP command line client
                # won't include them in its listing either.
                names = [
                    name
                    for name in names
                    if not (
                        name.startswith(".")
                        or
                        # The login directory of `ftp.microsoft.com` contains
                        # this "hidden" entry that ftputil finds but not the
                        # FTP command line client.
                        name == "mscomtest"
                    )
                ]
                failure_message = "For server {}, directory {}: {} != {}".format(
                    server, initial_directory, names, canonical_names
                )
                assert names == canonical_names, failure_message
        finally:
            host.close()

    @pytest.mark.slow_test
    def test_servers(self):
        """
        Test all servers in `self.servers`.

        For each server, get the listings for the login directory and one other
        directory which is known to exist. Use different "spellings" to
        retrieve each list via ftputil and compare with the results gotten with
        the command line client.
        """
        for server, actual_initial_directory in self.servers:
            print("=== server:", server)
            for initial_directory, paths in self.paths_table:
                initial_directory = initial_directory.replace(
                    "DIR", actual_initial_directory
                )
                print(server, initial_directory)
                self.inner_test_server(server, initial_directory, paths)
