# -*- coding: utf-8 -*-
# Copyright 2018, CS GROUP - France, https://www.csgroup.eu/
#
# This file is part of EODAG project
#     https://www.github.com/CS-SI/EODAG
#
# 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 datetime as dt
import logging
import os
import re
import unittest
from collections import defaultdict
from contextlib import contextmanager
from importlib.resources import files as res_files
from tempfile import TemporaryDirectory
from typing import Optional, Tuple

import click
import responses
import shapely
from click.testing import CliRunner
from faker import Faker
from packaging import version

from eodag.api.collection import Collection, CollectionsList
from eodag.api.search_result import SearchResult
from eodag.utils import GENERIC_COLLECTION
from eodag.utils.dates import to_iso_utc_string
from eodag.utils.exceptions import UnsupportedProvider
from tests import TEST_RESOURCES_PATH
from tests.context import (
    DEFAULT_LIMIT,
    AuthenticationError,
    MisconfiguredError,
    NoMatchingCollection,
    download,
    eodag_cli,
    mock,
    search_crunch,
)
from tests.units import test_core
from tests.utils import no_blanks, write_eodag_conf_with_fake_credentials


class TestEodagCli(unittest.TestCase):
    @contextmanager
    def user_conf(self, conf_file="user_conf.yml", content=b"key: to unused conf"):
        """Utility method"""
        with self.runner.isolated_filesystem():
            with open(conf_file, "wb") as fd:
                fd.write(
                    content if isinstance(content, bytes) else content.encode("utf-8")
                )
            yield conf_file

    def setUp(self):
        super(TestEodagCli, self).setUp()
        self.runner = CliRunner()
        self.faker = Faker()

        # Mock home and eodag conf directory to tmp dir
        self.tmp_home_dir = TemporaryDirectory()
        self.expanduser_mock = mock.patch(
            "os.path.expanduser", autospec=True, return_value=self.tmp_home_dir.name
        )
        self.expanduser_mock.start()

        # create eodag conf dir in tmp home dir
        eodag_conf_dir = os.path.join(self.tmp_home_dir.name, ".config", "eodag")
        os.makedirs(eodag_conf_dir, exist_ok=False)
        # use empty config file with fake credentials in order to have full
        # list for tests and prevent providers to be pruned
        write_eodag_conf_with_fake_credentials(
            os.path.join(eodag_conf_dir, "eodag.yml")
        )

    def tearDown(self):
        super(TestEodagCli, self).tearDown()
        # stop Mock and remove tmp config dir
        self.expanduser_mock.stop()
        self.tmp_home_dir.cleanup()
        # reset logging
        logger = logging.getLogger("eodag")
        logger.handlers = []
        logger.level = 0

    def eodag_command(
        self, command_list: list = [], env: dict = {}
    ) -> Tuple[int, str, Optional[Exception]]:
        """Eodag command line

        :param command_list: command line
        :param env: environment variables
        :returns: exit_code, stdout, stderr
        """

        # Force str parameters
        str_command_list = list(map(lambda x: "{}".format(x), command_list))
        result = self.runner.invoke(eodag_cli, str_command_list, env=env)

        exit_code = 1
        output = ""
        error = RuntimeError(
            "runner.invoke does not return expected result object, given {}".format(
                type(result)
            )
        )
        if isinstance(result, click.testing.Result):
            exit_code = result.exit_code
            output = result.output
            error = result.exception

        return exit_code, output, error

    def test_eodag_without_args(self):
        """Calling eodag without arguments should print help message"""
        exit_code, output, _ = self.eodag_command()
        self.assertIn(
            "Usage: eodag-cli [OPTIONS] COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]...",
            output,
        )
        # Exit status 2 with no_args_is_help starting click >= 8.2.0
        self.assertIn(exit_code, (0, 2))

    def test_eodag_with_only_verbose_opt(self):
        """Calling eodag only with -v option should print error message"""
        exit_code, output, error = self.eodag_command(["-v"])
        self.assertIn("Error: Missing command.", output)
        self.assertNotEqual(exit_code, 0)
        self.assertIsInstance(error, SystemExit)

    def test_eodag_cli_version(self):
        """Calling eodag version should return the version"""
        _, output, error = self.eodag_command(["version"])
        version_str = re.search(r"eodag .* version (.+)\s*$", output).group(1)
        self.assertIsNone(error)
        self.assertGreater(version.parse(version_str), version.parse("2.0.0"))

    def test_eodag_search_without_args(self):
        """Calling eodag search subcommand without arguments should print help message and return error code"""  # noqa
        exit_code, output, error = self.eodag_command(["search"])
        with click.Context(search_crunch) as ctx:
            self.assertEqual(
                no_blanks(output),
                no_blanks(
                    "".join(
                        (
                            "Give me some work to do. See below for how to do that:",
                            ctx.get_help(),
                        )
                    )
                ),
            )
        self.assertNotEqual(exit_code, 0)
        self.assertIsInstance(error, SystemExit)

    def test_eodag_search_without_collection_arg(self):
        """Calling eodag search without -p | --collection should print the help message and return error code"""  # noqa
        start_date = self.faker.date()
        end_date = self.faker.date()
        exit_code, output, error = self.eodag_command(
            ["search", "-s", start_date, "-e", end_date]
        )
        with click.Context(search_crunch) as ctx:
            self.assertEqual(
                no_blanks(output),
                no_blanks(
                    "".join(
                        (
                            "Give me some work to do. See below for how to do that:",
                            ctx.get_help(),
                        )
                    )
                ),
            )
        self.assertNotEqual(exit_code, 0)
        self.assertIsInstance(error, SystemExit)

    def test_eodag_search_with_conf_file_inexistent(self):
        """Calling eodag search with --conf | -f set to a non-existent file should print error message"""  # noqa
        conf_file = "does_not_exist.yml"
        exit_code, output, error = self.eodag_command(
            ["search", "--conf", conf_file, "-p", "whatever"]
        )
        expect_output = "Error: Invalid value for '-f' / '--conf': Path '{}' does not exist.".format(  # noqa
            conf_file
        )
        self.assertTrue(expect_output in output or expect_output.replace("'", '"'))
        self.assertNotEqual(exit_code, 0)
        self.assertIsInstance(error, SystemExit)

    def test_eodag_search_with_max_cloud_out_of_range(self):
        """Calling eodag search with -c | --maxCloud set to a value < 0 or > 100 should print error message"""  # noqa
        with self.user_conf() as conf_file:
            for max_cloud in (110, -1):
                exit_code, output, error = self.eodag_command(
                    ["search", "--conf", conf_file, "-p", "whatever", "-c", max_cloud],
                )
                expect_output = (
                    "Error: Invalid value for '-c' / '--cloudCover': {} is not in the"
                    " valid range of 0 to 100."
                ).format(max_cloud)
                self.assertTrue(
                    expect_output in output or expect_output.replace("'", '"')
                )
                self.assertNotEqual(exit_code, 0)
                self.assertIsInstance(error, UnsupportedProvider)

    def test_eodag_search_bbox_invalid(self):
        """Calling eodag search with -b | --bbox set with less than 4 params should print error message"""  # noqa
        with self.user_conf() as conf_file:
            exit_code, output, error = self.eodag_command(
                ["search", "--conf", conf_file, "-p", "whatever", "-b", 1, 2]
            )
            self.assertIn("-b", output)
            self.assertIn("requires 4 arguments", output)
            self.assertNotEqual(exit_code, 0)
            self.assertIsInstance(error, SystemExit)

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_search_bbox_valid(self, dag):
        """Calling eodag search with --bbox argument valid"""
        with self.user_conf() as conf_file:
            collection = "whatever"
            exit_code, output, error = self.eodag_command(
                ["search", "--conf", conf_file, "-c", collection, "-b", 1, 43, 2, 44],
            )
            self.assertEqual(exit_code, 0)
            self.assertIn("Results stored at", output)
            self.assertIsNone(error)

            api_obj = dag.return_value
            api_obj.search.assert_called_once_with(
                provider=None,
                limit=DEFAULT_LIMIT,
                page=1,
                start_datetime=None,
                end_datetime=None,
                geometry={"lonmin": 1, "latmin": 43, "lonmax": 2, "latmax": 44},
                instruments=None,
                platform=None,
                constellation=None,
                collection=collection,
                id=None,
                locations=None,
                count=False,
                **{
                    "eo:cloud_cover": None,
                    "processing:level": None,
                    "eodag:sensor_type": None,
                },
            )

    def test_eodag_search_geom_wkt_invalid(self):
        """Calling eodag search with -g | --geom set with invalit WKT geometry string"""  # noqa
        with self.user_conf() as conf_file:
            exit_code, _, error = self.eodag_command(
                ["search", "--conf", conf_file, "-c", "whatever", "-g", "not a wkt"]
            )
            self.assertNotEqual(exit_code, 0)
            # GEOSException for shapely >= 2.0
            self.assertTrue(isinstance(error, shapely.errors.GEOSException))

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_search_geom_wkt_valid(self, dag):
        """Calling eodag search with --geom WKT argument valid"""
        with self.user_conf() as conf_file:
            collection = "whatever"
            exit_code, output, error = self.eodag_command(
                [
                    "search",
                    "--conf",
                    conf_file,
                    "-c",
                    collection,
                    "-g",
                    "POLYGON ((1 43, 1 44, 2 44, 2 43, 1 43))",
                ],
            )
            self.assertEqual(exit_code, 0)
            self.assertIn("Results stored at", output)
            self.assertIsNone(error)

            api_obj = dag.return_value
            api_obj.search.assert_called_once_with(
                provider=None,
                limit=DEFAULT_LIMIT,
                page=1,
                start_datetime=None,
                end_datetime=None,
                geometry="POLYGON ((1 43, 1 44, 2 44, 2 43, 1 43))",
                instruments=None,
                platform=None,
                constellation=None,
                collection=collection,
                id=None,
                locations=None,
                count=False,
                **{
                    "eo:cloud_cover": None,
                    "processing:level": None,
                    "eodag:sensor_type": None,
                },
            )

    def test_eodag_search_bbox_geom_mutually_exclusive(self):
        """Calling eodag search with both --geom and --box"""  # noqa
        with self.user_conf() as conf_file:
            exit_code, output, error = self.eodag_command(
                [
                    "search",
                    "--conf",
                    conf_file,
                    "-p",
                    "whatever",
                    "-b",
                    1,
                    2,
                    3,
                    4,
                    "-g",
                    "a wkt",
                ]
            )
            self.assertIn("Illegal usage", output)
            self.assertNotEqual(exit_code, 0)
            self.assertIsInstance(error, SystemExit)

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_search_storage_arg(self, dag):
        """Calling eodag search with specified result filename without .geojson extension"""  # noqa
        with self.user_conf() as conf_file:
            api_obj = dag.return_value
            api_obj.search.return_value = SearchResult([mock.MagicMock() * 2], 2)
            exit_code, output, error = self.eodag_command(
                [
                    "search",
                    "--conf",
                    conf_file,
                    "-c",
                    "whatever",
                    "--storage",
                    "results",
                ]
            )
            self.assertEqual(exit_code, 0)
            self.assertIn("Results stored at", output)
            self.assertIsNone(error)

            api_obj.serialize.assert_called_with(
                api_obj.search.return_value, filename="results.geojson"
            )

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_search_with_cruncher(self, dag):
        """Calling eodag search with --cruncher arg should call crunch method of search result"""  # noqa
        with self.user_conf() as conf_file:
            api_obj = dag.return_value
            api_obj.search.return_value = SearchResult([mock.MagicMock() * 2], 2)

            collection = "whatever"
            cruncher = "FilterLatestIntersect"
            criteria = dict(
                provider=None,
                start_datetime=None,
                end_datetime=None,
                geometry=None,
                instruments=None,
                platform=None,
                constellation=None,
                collection=collection,
                id=None,
                locations=None,
                **{
                    "eo:cloud_cover": None,
                    "processing:level": None,
                    "eodag:sensor_type": None,
                },
            )
            exit_code, output, error = self.eodag_command(
                ["search", "-f", conf_file, "-c", collection, "--cruncher", cruncher]
            )
            self.assertEqual(exit_code, 0)
            self.assertIn("Results stored at", output)
            self.assertIsNone(error)

            search_results = api_obj.search.return_value
            crunch_results = api_obj.crunch.return_value

            # Assertions
            dag.assert_called_once_with(
                user_conf_file_path=conf_file, locations_conf_path=None
            )
            api_obj.search.assert_called_once_with(
                count=False, limit=DEFAULT_LIMIT, page=1, **criteria
            )
            api_obj.crunch.assert_called_once_with(
                search_results, search_criteria=criteria, **{cruncher: {}}
            )
            api_obj.serialize.assert_called_with(
                crunch_results, filename="search_results.geojson"
            )

            # Call with a cruncher taking arguments
            cruncher = "FilterOverlap"
            exit_code, output, error = self.eodag_command(
                [
                    "search",
                    "-f",
                    conf_file,
                    "-c",
                    collection,
                    "--cruncher",
                    cruncher,
                    "--cruncher-args",
                    cruncher,
                    "minimum_overlap",
                    "10",
                ]
            )

            self.assertEqual(exit_code, 0)
            self.assertIn("Results stored at", output)
            self.assertIsNone(error)

            api_obj.crunch.assert_called_with(
                search_results,
                search_criteria=criteria,
                **{cruncher: {"minimum_overlap": "10"}},
            )

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_search_and_download(self, dag):
        """Calling eodag search with --download argument should directly download search results"""
        with self.user_conf() as conf_file:
            api_obj = dag.return_value
            search_results = api_obj.search.return_value = SearchResult(
                [mock.MagicMock() * 2], 2
            )

            collection = "whatever"
            criteria = dict(
                provider=None,
                start_datetime=None,
                end_datetime=None,
                geometry=None,
                instruments=None,
                platform=None,
                constellation=None,
                collection=collection,
                id=None,
                locations=None,
                **{
                    "eo:cloud_cover": None,
                    "processing:level": None,
                    "eodag:sensor_type": None,
                },
            )
            self.runner.invoke(
                eodag_cli,
                ["search", "-f", conf_file, "-c", collection, "download"],
            )

            # Assertions
            self.assertEqual(dag.call_count, 2)
            dag.assert_any_call(user_conf_file_path=conf_file, locations_conf_path=None)
            api_obj.search.assert_called_once_with(
                count=False, limit=DEFAULT_LIMIT, page=1, **criteria
            )
            api_obj.download_all.assert_called_once_with(
                search_results, output_dir=None, executor=mock.ANY
            )

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_search_all(self, dag):
        """Calling eodag search with --bbox argument valid"""
        with self.user_conf() as conf_file:
            collection = "whatever"
            exit_code, output, error = self.eodag_command(
                [
                    "search",
                    "--conf",
                    conf_file,
                    "-c",
                    collection,
                    "-g",
                    "POLYGON ((1 43, 1 44, 2 44, 2 43, 1 43))",
                    "--all",
                ]
            )
            self.assertEqual(exit_code, 0)
            self.assertIn("Results stored at", output)
            self.assertIsNone(error)

            api_obj = dag.return_value
            api_obj.search_all.assert_called_once_with(
                provider=None,
                limit=None,
                start_datetime=None,
                end_datetime=None,
                geometry="POLYGON ((1 43, 1 44, 2 44, 2 43, 1 43))",
                instruments=None,
                platform=None,
                constellation=None,
                collection=collection,
                id=None,
                locations=None,
                **{
                    "eo:cloud_cover": None,
                    "processing:level": None,
                    "eodag:sensor_type": None,
                },
            )

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_search_query(self, dag):
        """Calling eodag search with --query argument"""
        with self.user_conf() as conf_file:
            collection = "whatever"
            exit_code, output, error = self.eodag_command(
                [
                    "search",
                    "--conf",
                    conf_file,
                    "-c",
                    collection,
                    "--query",
                    "foo=1&bar=2&bar=3",
                ]
            )
            self.assertEqual(exit_code, 0)
            self.assertIn("Results stored at", output)
            self.assertIsNone(error)

            api_obj = dag.return_value
            api_obj.search.assert_called_once_with(
                provider=None,
                page=1,
                limit=20,
                geometry=None,
                start_datetime=None,
                end_datetime=None,
                collection=collection,
                instruments=None,
                platform=None,
                constellation=None,
                id=None,
                foo="1",
                bar=["2", "3"],
                locations=None,
                count=False,
                **{
                    "eo:cloud_cover": None,
                    "processing:level": None,
                    "eodag:sensor_type": None,
                },
            )

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_search_locations(self, dag):
        """Calling eodag search with --locations argument"""
        with self.user_conf() as conf_file:
            collection = "whatever"
            exit_code, output, error = self.eodag_command(
                [
                    "search",
                    "--conf",
                    conf_file,
                    "-c",
                    collection,
                    "--locations",
                    "country=FRA&continent=Africa",
                ]
            )
            self.assertEqual(exit_code, 0)
            self.assertIn("Results stored at", output)
            self.assertIsNone(error)

            api_obj = dag.return_value
            api_obj.search.assert_called_once_with(
                provider=None,
                page=1,
                limit=20,
                geometry=None,
                start_datetime=None,
                end_datetime=None,
                collection=collection,
                instruments=None,
                platform=None,
                constellation=None,
                id=None,
                locations={"country": "FRA", "continent": "Africa"},
                count=False,
                **{
                    "eo:cloud_cover": None,
                    "processing:level": None,
                    "eodag:sensor_type": None,
                },
            )

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_search_start_date(self, dag):
        """Calling eodag search with --start argument"""
        with self.user_conf() as conf_file:
            collection = "whatever"
            start_date_str = "2022-01-01"
            start_date_datetime = to_iso_utc_string(
                dt.datetime.strptime(start_date_str, "%Y-%m-%d")
            )
            exit_code, output, error = self.eodag_command(
                [
                    "search",
                    "--conf",
                    conf_file,
                    "-c",
                    collection,
                    "--start",
                    start_date_str,
                ]
            )
            self.assertEqual(exit_code, 0)
            self.assertIn("Results stored at", output)
            self.assertIsNone(error)

            api_obj = dag.return_value
            api_obj.search.assert_called_once_with(
                provider=None,
                page=1,
                limit=20,
                geometry=None,
                start_datetime=start_date_datetime,
                end_datetime=None,
                collection=collection,
                instruments=None,
                platform=None,
                constellation=None,
                id=None,
                locations=None,
                count=False,
                **{
                    "eo:cloud_cover": None,
                    "processing:level": None,
                    "eodag:sensor_type": None,
                },
            )

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_search_stop_date(self, dag):
        """Calling eodag search with --end argument"""
        with self.user_conf() as conf_file:
            collection = "whatever"
            stop_date_str = "2022-01-01"
            stop_date_datetime = to_iso_utc_string(
                dt.datetime.strptime(stop_date_str, "%Y-%m-%d")
            )
            exit_code, output, error = self.eodag_command(
                [
                    "search",
                    "--conf",
                    conf_file,
                    "-c",
                    collection,
                    "--end",
                    stop_date_str,
                    "--count",
                ]
            )
            self.assertEqual(exit_code, 0)
            self.assertIn("Results stored at", output)
            self.assertIsNone(error)

            api_obj = dag.return_value
            api_obj.search.assert_called_once_with(
                provider=None,
                page=1,
                limit=20,
                geometry=None,
                start_datetime=None,
                end_datetime=stop_date_datetime,
                collection=collection,
                instruments=None,
                platform=None,
                constellation=None,
                id=None,
                locations=None,
                count=True,
                **{
                    "eo:cloud_cover": None,
                    "processing:level": None,
                    "eodag:sensor_type": None,
                },
            )

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_search_locs(self, dag):
        """Calling eodag search with --locs argument"""
        with self.user_conf() as conf_file:
            locs_file = os.path.join(TEST_RESOURCES_PATH, "file_locations_override.yml")

            collection = "whatever"
            exit_code, output, error = self.eodag_command(
                [
                    "search",
                    "--conf",
                    conf_file,
                    "--locs",
                    locs_file,
                    "-c",
                    collection,
                ]
            )
            self.assertEqual(exit_code, 0)
            self.assertIn("Results stored at", output)
            self.assertIsNone(error)

            dag.assert_called_once_with(
                user_conf_file_path=conf_file,
                locations_conf_path=locs_file,
            )

    def test_eodag_list_collection_ok(self):
        """Calling eodag list without provider should return all supported collections"""
        all_supported_collections = [
            col
            for col, provs in test_core.TestCore.SUPPORTED_COLLECTIONS.items()
            if len(provs) != 0 and col != GENERIC_COLLECTION
        ]
        exit_code, output, error = self.eodag_command(["list", "--no-fetch"])
        self.assertEqual(exit_code, 0)
        self.assertIsNone(error)
        for col in all_supported_collections:
            self.assertIn(col, output)

    def test_eodag_list_collection_with_provider_ok(self):
        """Calling eodag list with provider should return all supported collections of specified provider"""  # noqa
        for provider in test_core.TestCore.SUPPORTED_PROVIDERS:
            provider_supported_collections = [
                col
                for col, provs in test_core.TestCore.SUPPORTED_COLLECTIONS.items()
                if provider in provs
                if col != GENERIC_COLLECTION
            ]
            exit_code, output, error = self.eodag_command(
                ["list", "-p", provider, "--no-fetch"]
            )
            self.assertEqual(exit_code, 0)
            self.assertIsNone(error)
            for col in provider_supported_collections:
                self.assertIn(
                    col,
                    output,
                    f"{col} was not found in {provider} supported collections",
                )

    def test_eodag_list_collection_with_provider_ko(self):
        """Calling eodag list with unsupported provider should fail and print a list of available providers"""  # noqa
        provider = "random"
        exit_code, output, error = self.eodag_command(
            ["list", "-p", provider, "--no-fetch"]
        )
        self.assertEqual(exit_code, 1)
        self.assertIsInstance(error, SystemExit)
        self.assertIn("Unsupported provider. You may have a typo", output)
        self.assertIn(
            f"Available providers: {', '.join(test_core.TestCore.SUPPORTED_PROVIDERS)}",
            output,
        )

    @mock.patch(
        "eodag.api.core.EODataAccessGateway.fetch_collections_list", autospec=True
    )
    def test_eodag_list_collection_fetch(self, mock_fetch_collections_list):
        """Calling eodag list should fetch for new collections depending on passed option"""

        exit_code, output, error = self.eodag_command(["list", "--no-fetch"])
        self.assertEqual(exit_code, 0)
        self.assertIn("Listing available collections:", output)
        self.assertIsNone(error)

        assert not mock_fetch_collections_list.called

        exit_code, output, error = self.eodag_command(["list"])
        self.assertEqual(exit_code, 0)
        self.assertIn("Listing available collections:", output)
        self.assertIsNone(error)

        mock_fetch_collections_list.assert_called_once_with(mock.ANY, provider=None)

        exit_code, output, error = self.eodag_command(["list", "-p", "cop_dataspace"])
        self.assertEqual(exit_code, 0)
        self.assertIn("Listing available collections:", output)
        self.assertIsNone(error)

        mock_fetch_collections_list.assert_called_with(
            mock.ANY, provider="cop_dataspace"
        )
        self.assertEqual(mock_fetch_collections_list.call_count, 2)

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_guess_collection_ok(self, dag):
        """Calling eodag list with one or several valid collection feature(s) should return
        all supported collections with this (these) feature(s) among the ones of its provider
        and with or without fetching provider according to the command.
        """
        provider = "cop_dataspace"

        dag.return_value.guess_collection.return_value = CollectionsList(
            [
                Collection.create_with_dag(dag=dag, id="foo", title="this is foo"),
                Collection.create_with_dag(dag=dag, id="bar", title="this is bar"),
            ]
        )
        dag.return_value.list_collections.return_value = CollectionsList(
            [
                Collection.create_with_dag(dag=dag, id="foo", title="this is foo"),
                Collection.create_with_dag(dag=dag, id="bar", title="this is bar"),
                Collection.create_with_dag(dag=dag, id="baz", title="this is baz"),
            ]
        )
        exit_code, output, error = self.eodag_command(
            ["list", "-p", provider, "--platform", "S2A", "--no-fetch"]
        )
        self.assertEqual(exit_code, 0)
        self.assertIsNone(error)

        self.assertIn("foo", output)
        self.assertIn("bar", output)
        self.assertNotIn("baz", output)

        dag.return_value.list_collections.assert_called_with(
            provider=provider, fetch_providers=False
        )

    @mock.patch(
        "eodag.api.core.EODataAccessGateway.guess_collection",
        autospec=True,
        side_effect=NoMatchingCollection(),
    )
    def test_eodag_guess_collection_ko(self, mock_guess_collection):
        """Calling eodag list with invalid collection feature(s) should print a
        'no matching' type message and return error code.
        """
        exit_code, output, error = self.eodag_command(
            ["list", "--platform", "fake_identifier", "--no-fetch"]
        )
        self.assertNotEqual(exit_code, 0)
        self.assertIsInstance(error, SystemExit)
        self.assertIn(
            "No collection match the following criteria you provided:\n",
            output,
        )

    @mock.patch(
        "eodag.api.core.EODataAccessGateway.discover_collections",
        autospec=True,
        return_value={},
    )
    def test_eodag_discover_collections(self, mock_discover_collections):
        """Calling eodag discover should fetch providers for new collections"""
        origin_dir = os.getcwd()
        try:
            os.chdir(self.tmp_home_dir.name)
            default_output_path = os.path.join(
                self.tmp_home_dir.name, "ext_collections.json"
            )
            self.assertFalse(os.path.isfile(default_output_path))

            # call without any arg
            exit_code, output, error = self.eodag_command(["discover"])
            self.assertEqual(exit_code, 0)
            self.assertIsNone(error)
            self.assertIn("Results stored at", output)

            mock_discover_collections.assert_called_once_with(mock.ANY)
            self.assertTrue(os.path.isfile(default_output_path))

            # call with provider
            exit_code, output, error = self.eodag_command(
                ["discover", "-p", "cop_dataspace"]
            )
            self.assertEqual(exit_code, 0)
            self.assertIsNone(error)
            self.assertIn("Results stored at", output)

            mock_discover_collections.assert_called_with(
                mock.ANY, provider="cop_dataspace"
            )
            self.assertEqual(mock_discover_collections.call_count, 2)
            os.remove(default_output_path)

            other_file_path = os.path.join(self.tmp_home_dir.name, "toto")
            # call with filename without extension
            self.assertFalse(os.path.isfile(other_file_path))
            self.assertFalse(os.path.isfile(f"{other_file_path}.json"))

            exit_code, output, error = self.eodag_command(
                ["discover", "--storage", "toto"]
            )
            self.assertEqual(exit_code, 0)
            self.assertIsNone(error)
            self.assertIn("Results stored at 'toto.json'", output)

            mock_discover_collections.assert_called_with(mock.ANY)
            self.assertEqual(mock_discover_collections.call_count, 3)
            self.assertFalse(os.path.isfile(other_file_path))
            self.assertTrue(os.path.isfile(f"{other_file_path}.json"))
            os.remove(f"{other_file_path}.json")

            # call with filename with extension
            self.assertFalse(os.path.isfile(f"{other_file_path}.json"))

            exit_code, output, error = self.eodag_command(
                ["discover", "--storage", "toto.json"]
            )
            self.assertEqual(exit_code, 0)
            self.assertIsNone(error)
            self.assertIn("Results stored at 'toto.json'", output)

            mock_discover_collections.assert_called_with(mock.ANY)
            self.assertEqual(mock_discover_collections.call_count, 4)
            self.assertTrue(os.path.isfile(f"{other_file_path}.json"))
            os.remove(f"{other_file_path}.json")
        finally:
            os.chdir(origin_dir)

    def test_eodag_download_no_search_results_arg(self):
        """Calling eodag download without a path to a search result should fail"""
        exit_code, output, error = self.eodag_command(["download"])
        self.assertEqual(exit_code, 1)
        self.assertIsInstance(error, SystemExit)

        with click.Context(download) as ctx:
            self.assertEqual(
                no_blanks(output),
                no_blanks(
                    "".join(
                        (
                            "Nothing to do (no search results file or stac item provided)",
                            ctx.get_help(),
                        )
                    )
                ),
            )

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_download_ok(self, dag):
        """Calling eodag download with all args well formed succeed"""
        search_results_path = os.path.join(
            TEST_RESOURCES_PATH, "eodag_search_result.geojson"
        )
        config_path = os.path.join(TEST_RESOURCES_PATH, "file_config_override.yml")
        dag.return_value.download_all.return_value = ["/fake_path"]

        exit_code, output, error = self.eodag_command(
            ["download", "--search-results", search_results_path, "-f", config_path],
        )
        self.assertEqual(exit_code, 0)
        self.assertIsNone(error)
        self.assertIn("Downloaded", output)

        dag.assert_called_once_with(user_conf_file_path=config_path)
        dag.return_value.deserialize_and_register.assert_called_once_with(
            search_results_path
        )
        self.assertEqual(dag.return_value.download_all.call_count, 1)
        self.assertEqual("Downloaded /fake_path\n", output)

        # Testing the case when no downloaded path is returned
        dag.return_value.download_all.return_value = [None]
        exit_code, output, error = self.eodag_command(
            ["download", "--search-results", search_results_path, "-f", config_path],
        )
        self.assertEqual(exit_code, 0)
        self.assertIsNone(error)
        self.assertEqual(
            "A file may have been downloaded but we cannot locate it\n", output
        )

        # Testing output directory
        output_dir = os.path.join(self.tmp_home_dir.name, "downloads")
        exit_code, output, error = self.eodag_command(
            [
                "download",
                "--search-results",
                search_results_path,
                "-f",
                config_path,
                "--output-dir",
                output_dir,
            ],
        )
        self.assertEqual(exit_code, 0)
        self.assertIsNone(error)
        self.assertIn("A file may have been downloaded but we cannot locate it", output)

        dag.return_value.download_all.assert_called_with(
            mock.ANY, output_dir=output_dir, executor=mock.ANY
        )

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_download_ko(self, dag):
        """Calling eodag download with all args well formed fails"""
        search_results_path = os.path.join(
            TEST_RESOURCES_PATH, "eodag_search_result.geojson"
        )
        config_path = os.path.join(TEST_RESOURCES_PATH, "file_config_override.yml")
        dag.return_value.download_all.return_value = []
        exit_code, output, error = self.eodag_command(
            ["download", "--search-results", search_results_path, "-f", config_path],
        )
        self.assertEqual(exit_code, 0)
        self.assertIsNone(error)
        self.assertEqual(
            "Error during download, a file may have been downloaded but we cannot locate it\n",
            output,
        )

    @mock.patch("eodag.api.core.EODataAccessGateway", autospec=True)
    def test_eodag_download_stac_items(self, dag):
        """Calling eodag download with --stac-item argument"""
        fake_result = SearchResult([mock.MagicMock() * 2], 2)
        dag.return_value.import_stac_items.return_value = fake_result
        with self.user_conf() as conf_file:
            exit_code, output, error = self.eodag_command(
                [
                    "download",
                    "--conf",
                    conf_file,
                    "--stac-item",
                    "foo",
                    "--stac-item",
                    "bar",
                ],
            )
            self.assertEqual(exit_code, 0)
            self.assertIsNone(error)
            self.assertIn(
                "Error during download, a file may have been downloaded but we cannot locate it",
                output,
            )

            dag.return_value.import_stac_items.assert_called_once_with(
                ["foo", "bar"],
            )
            dag.return_value.download_all.assert_called_once_with(
                fake_result, output_dir=None, executor=mock.ANY
            )

    @mock.patch(
        "eodag.plugins.authentication.openid_connect.requests.sessions.Session.request",
        autospec=True,
    )
    def test_eodag_download_missingcredentials(self, oidc_request):
        """Calling eodag download with missing credentials must raise MisconfiguredError"""
        # mocked OIDC discovery response as expected by pyjwt
        oidc_request.return_value.json.return_value = {
            "jwks_uri": "https://example.com/jwks",
            "token_endpoint": "https://example.com/token",
            "authorization_endpoint": "https://example.com/authorize",
            "id_token_signing_alg_values_supported": ["RS256"],
        }
        search_results_path = os.path.join(
            TEST_RESOURCES_PATH, "eodag_search_result.geojson"
        )
        default_conf_file = str(
            res_files("eodag") / "resources" / "user_conf_template.yml"
        )
        exit_code, _, error = self.eodag_command(
            [
                "download",
                "--search-results",
                search_results_path,
                "-f",
                default_conf_file,
            ]
        )
        self.assertEqual(exit_code, 1)
        self.assertIsInstance(error, MisconfiguredError)

    @mock.patch(
        "eodag.plugins.authentication.openid_connect.OIDCRefreshTokenBase._get_oidc_endpoints",
        autospec=True,
    )
    @responses.activate
    def test_eodag_download_wrongcredentials(self, mock_req_oidc_endpoints):
        """Calling eodag download with wrong credentials must raise AuthenticationError"""
        oidc_endpoints = defaultdict(mock.Mock())
        oidc_endpoints["token_endpoint"] = "http://fake_token_endpoint"
        # mocked OIDC discovery response as expected by pyjwt
        oidc_endpoints["jwks_uri"] = "https://example.com/jwks"
        mock_req_oidc_endpoints.return_value = oidc_endpoints
        responses.add(
            responses.POST,
            "http://fake_token_endpoint",
            status=401,
        )

        search_results_path = os.path.join(
            TEST_RESOURCES_PATH, "eodag_search_result.geojson"
        )
        exit_code, _, error = self.eodag_command(
            ["download", "--search-results", search_results_path],
            # We override the default (empty) credentials with dummy values not
            # to raise a MisconfiguredError.
            env={
                "EODAG__COP_DATASPACE__AUTH__CREDENTIALS__USERNAME": "dummy",
                "EODAG__COP_DATASPACE__AUTH__CREDENTIALS__PASSWORD": "dummy",
            },
        )
        self.assertEqual(exit_code, 1)
        self.assertIsInstance(error, AuthenticationError)

    @mock.patch(
        "eodag.plugins.authentication.openid_connect.requests.get", autospec=True
    )
    @mock.patch("eodag.api.product._product.EOProduct.get_quicklook", autospec=True)
    def test_eodag_download_quicklooks(self, mock_get_quicklook, mock_oidc_get):
        """Calling eodag download with --quicklooks argument"""
        # mocked OIDC discovery response as expected by pyjwt
        mock_oidc_get.return_value.json.return_value = {
            "jwks_uri": "https://example.com/jwks",
            "token_endpoint": "https://example.com/token",
            "authorization_endpoint": "https://example.com/authorize",
            "id_token_signing_alg_values_supported": ["RS256"],
        }
        search_results_path = os.path.join(
            TEST_RESOURCES_PATH, "eodag_search_result_cop_dataspace.geojson"
        )
        config_path = os.path.join(TEST_RESOURCES_PATH, "file_config_override.yml")
        mock_get_quicklook.return_value = "/fake_path"
        exit_code, output, error = self.eodag_command(
            [
                "download",
                "--search-results",
                search_results_path,
                "-f",
                config_path,
                "--quicklooks",
            ],
        )
        self.assertEqual(exit_code, 0)
        self.assertIsNone(error)
        self.assertIn(
            "Flag 'quicklooks' specified, downloading only quicklooks of products\n",
            output,
        )

        # 2 quicklooks are called in search_results_path
        self.assertEqual(mock_get_quicklook.call_count, 2)
        self.assertIn("Downloaded /fake_path\n", output)

        # change output dir
        output_dir = os.path.join(self.tmp_home_dir.name, "quicklooks")
        exit_code, output, error = self.eodag_command(
            [
                "download",
                "--search-results",
                search_results_path,
                "-f",
                config_path,
                "--quicklooks",
                "--output-dir",
                output_dir,
            ]
        )
        self.assertEqual(exit_code, 0)
        self.assertIsNone(error)
        self.assertIn("Downloaded", output)

        mock_get_quicklook.assert_called_with(mock.ANY, output_dir=output_dir)

        # Testing the case when no quicklook path is returned
        mock_get_quicklook.return_value = None
        exit_code, output, error = self.eodag_command(
            [
                "download",
                "--search-results",
                search_results_path,
                "-f",
                config_path,
                "--quicklooks",
            ],
        )
        self.assertEqual(exit_code, 0)
        self.assertIsNone(error)
        self.assertIn(
            "A quicklook may have been downloaded but we cannot locate it. "
            "Increase verbosity for more details: `eodag -v download [OPTIONS]`",
            output,
        )
