# Copyright © The Debusine Developers
# See the AUTHORS file at the top-level directory of this distribution
#
# This file is part of Debusine. It is subject to the license terms
# in the LICENSE file found in the top-level directory of this
# distribution. No part of Debusine, including this file, may be copied,
# modified, propagated, or distributed except according to the terms
# contained in the LICENSE file.

"""Common infrastructure for testing the debusine Cli commands."""

import argparse
import logging
from collections.abc import Callable
from typing import Any
from unittest import mock
from unittest.mock import MagicMock

from debusine.client.cli import Cli
from debusine.client.commands.base import Command, DebusineCommand
from debusine.client.debusine import Debusine
from debusine.client.tests import base


class BaseCliTests(base.BaseCliTests):
    """Basic functionality to implement tests for the Cli class."""

    def build_parsed_namespace(self, **kwargs: Any) -> argparse.Namespace:
        """
        Build a DebusineCommand for tests.

        :param kwargs: key-value pairs to set in the mock argparse namespace
          passed to the DebusineCommand
        """
        kwargs.setdefault("server", None)
        kwargs.setdefault("scope", None)
        kwargs.setdefault("silent", False)
        kwargs.setdefault("debug", False)
        kwargs.setdefault("yaml", False)
        if "config_file" not in kwargs:  # pragma: no cover
            kwargs["config_file"] = self.create_config_file().as_posix()

        args = argparse.Namespace()
        for k, v in kwargs.items():
            setattr(args, k, v)

        return args

    def command[C: Command](self, command_class: type[C], **kwargs: Any) -> C:
        """Create an argumentless constructor function for a command class."""
        parser = Cli.build_argument_parser()
        namespace = self.build_parsed_namespace(**kwargs)
        return command_class(parser, namespace)

    def patch_build_debusine_object(self) -> MagicMock:
        """
        Patch _build_debusine_object. Return mock.

        The mocked object return_value is a MagicMock(spec=Debusine).

        Keyword arguments correspond to methods of the resulting Debusine
        object to mock.
        """
        patcher = mock.patch.object(
            DebusineCommand, "_build_debusine_object", autospec=True
        )
        mocked = patcher.start()
        mock_debusine = mock.create_autospec(spec=Debusine)
        mock_debusine._logger = logging.getLogger("debusine.client.tests")
        mocked.return_value = mock_debusine
        self.addCleanup(patcher.stop)

        return mocked

    def assertShowsError(self, func: Callable[[], None]) -> Exception:
        """
        Call the function and ensures it calls show_error.

        :returns: the exception passed to show_error.
        """
        with mock.patch(
            "debusine.client.commands.base.Command.show_error",
            side_effect=SystemExit(42),
        ) as show_error:
            stderr, stdout = self.capture_output(
                func, assert_system_exit_code=42
            )
        show_error.assert_called_once()
        self.assertEqual(stderr, "")
        self.assertEqual(stdout, "")
        exception = show_error.call_args[0][0]
        assert isinstance(exception, Exception)
        return exception
