# 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.

"""Tests for the debusine Cli WorkflowTemplate commands."""

import abc
from functools import partial
from typing import Any
from unittest import mock

import yaml

from debusine.client.commands.base import Command, DebusineCommand
from debusine.client.commands.tests.base import BaseCliTests
from debusine.client.commands.workflow_templates import WorkflowTemplateCommand
from debusine.client.debusine import Debusine
from debusine.client.exceptions import DebusineError
from debusine.client.models import WorkflowTemplateData, WorkflowTemplateDataNew


class WorkflowTemplateCommandTests(BaseCliTests):
    """Tests for the :py:class:`CollectionCommand` class."""

    def setUp(self) -> None:
        super().setUp()
        self.data = WorkflowTemplateData(
            id=42,
            name="sample",
            task_name="noop",
            task_data={"test": 2},
            priority=42,
        )

    @Command.preserve_registry()
    def _command(self, **kwargs: Any) -> WorkflowTemplateCommand:

        class ConcreteWorkflowTemplateCommand(WorkflowTemplateCommand):
            """Version of WorkflowTemplateCommand that can be instantiated."""

            def run(self) -> None:
                raise NotImplementedError()

        return ConcreteWorkflowTemplateCommand(
            self.build_parsed_namespace(workspace="workspace")
        )

    def test_list_rich(self) -> None:
        command = self._command(workspace="workspace")
        with (
            mock.patch(
                "debusine.client.commands.workflow_templates.Table",
                return_value=(table := mock.MagicMock()),
            ),
            mock.patch(
                "debusine.client.commands.workflow_templates.rich.print"
            ) as rich_print,
        ):
            stderr, stdout = self.capture_output(
                partial(command._list_rich, [self.data])
            )

        self.assertEqual(stderr, "")
        self.assertEqual(stdout, "")
        rich_print.assert_called_once()

        self.assertEqual(table.add_row.call_count, 1)
        url = (
            "https://debusine.debian.org/debian/workspace/"
            "workflow-template/sample/"
        )
        call = table.add_row.call_args_list[0]
        self.assertEqual(call.args[0], "42")
        self.assertEqual(call.args[1], f"[link={url}]sample[/]")
        self.assertEqual(call.args[2], "noop")
        self.assertEqual(call.args[3], "42")

    def test_show_rich(self) -> None:
        command = self._command(workspace="workspace")
        with (
            mock.patch(
                "debusine.client.commands.workflow_templates.Table",
                return_value=(table := mock.MagicMock()),
            ),
            mock.patch(
                "debusine.client.commands.workflow_templates.rich.print"
            ) as rich_print,
        ):
            stderr, stdout = self.capture_output(
                partial(command._show_rich, self.data)
            )

        self.assertEqual(stderr, "")
        self.assertEqual(stdout, "")
        rich_print.assert_called_once()

        self.assertEqual(table.add_row.call_count, 5)
        url = (
            "https://debusine.debian.org/debian/workspace/"
            "workflow-template/sample/"
        )
        rows = [
            call_args_list.args
            for call_args_list in table.add_row.call_args_list
        ]
        self.assertEqual(rows[0], ("ID:", "#42"))
        self.assertEqual(rows[1], ("Name:", f"[link={url}]sample[/]"))
        self.assertEqual(rows[2], ("Task name:", "noop"))
        self.assertEqual(rows[3], ("Priority:", "42"))


class CreateTestsBase(BaseCliTests, abc.ABC):
    """Base tests for the CLI workflow_template create commands."""

    base_command: list[str]

    @abc.abstractmethod
    def assertStdoutYaml(
        self, workspace: str, stdout: str, result: WorkflowTemplateData
    ) -> None:
        """Ensure the program stdout matches the given result."""

    @abc.abstractmethod
    def command(
        self, task_name: str, template_name: str, *args: str
    ) -> list[str]:
        """Build a command line."""

    def assert_create_success(
        self,
        task_name: str,
        template_name: str,
        *args: str,
        workspace: str,
        data: WorkflowTemplateDataNew,
    ) -> None:
        """
        Call workflow-template create with the given arguments.

        :param args: arguments to append to
          ``debusine workflow-template create``
        :param data: data expected to be sent to the server
        """
        command = self.create_command(
            self.command(task_name, template_name, "--yaml", *args)
        )
        assert isinstance(command, DebusineCommand)
        result = WorkflowTemplateData(id=42, **data.dict())
        with mock.patch.object(
            command.debusine, "workflow_template_create", return_value=result
        ) as create:
            stderr, stdout = self.capture_output(command.run)

        create.assert_called_with(workspace, data)
        self.assertEqual(stderr, "")
        self.assertStdoutYaml(workspace, stdout, result)

    def test_invalid_task_name(self) -> None:
        """CLI fails if the task name is bad."""
        task_name = "task-name"

        self.enterContext(self.patch_sys_stdin_read("{}"))
        cli = self.create_cli(self.command(task_name, "test"))

        with mock.patch.object(
            Debusine,
            "workflow_template_create",
            autospec=True,
            side_effect=DebusineError(title=f"invalid {task_name}"),
        ):
            exception = self.assertShowsError(cli.execute)

        self.assertDebusineError(exception, {"title": f"invalid {task_name}"})

    def test_specific_workspace(self) -> None:
        """CLI parses the command line and uses --workspace."""
        self.enterContext(self.patch_sys_stdin_read("{}"))
        self.assert_create_success(
            "sbuild",
            "sbuild-test",
            "--workspace",
            "Testing",
            workspace="Testing",
            data=WorkflowTemplateDataNew(
                name="sbuild-test", task_name="sbuild", task_data={}
            ),
        )

    def test_success_data_from_file(self) -> None:
        """CLI creates a workflow template with data from a file."""
        tempfile = self.create_temporary_file()
        tempfile.write_text("{}")
        self.assert_create_success(
            "sbuild",
            "sbuild-test",
            f"--data={tempfile}",
            workspace="developers",
            data=WorkflowTemplateDataNew(
                name="sbuild-test", task_name="sbuild", task_data={}
            ),
        )

    def test_success_data_from_stdin(self) -> None:
        """CLI creates a workflow template with data from stdin."""
        self.enterContext(self.patch_sys_stdin_read("{}"))
        self.assert_create_success(
            "sbuild",
            "sbuild-test",
            workspace="developers",
            data=WorkflowTemplateDataNew(
                name="sbuild-test", task_name="sbuild", task_data={}
            ),
        )

    def test_data_is_empty(self) -> None:
        """CLI rejects a workflow template with empty data."""
        empty_file = self.create_temporary_file()
        cli = self.create_cli(
            self.command("sbuild", "sbuild-test", "--data", str(empty_file))
        )

        stderr, stdout = self.capture_output(
            cli.execute, assert_system_exit_code=3
        )

        self.assertEqual(
            stderr, "Error: data must be a dictionary. It is empty\n"
        )
        self.assertEqual(stdout, "")

    def test_yaml_errors_failed(self) -> None:
        """cli.execute() deals with different invalid task_data."""
        workflow_templates = [
            {
                "task_data": (
                    "test:\n"
                    "  name: a-name\n"
                    "    first-name: some first name"
                ),
                "comment": "yaml.safe_load raises ScannerError",
            },
            {
                "task_data": "input:\n  source_url: https://example.com\n )",
                "comment": "yaml.safe_load raises ParserError",
            },
        ]

        for workflow_template in workflow_templates:
            task_data = workflow_template["task_data"]
            with self.subTest(task_data), self.patch_sys_stdin_read(task_data):
                cli = self.create_cli(
                    self.command("task-name", "template-name")
                )
                stderr, stdout = self.capture_output(
                    cli.execute, assert_system_exit_code=3
                )

                self.assertRegex(stderr, "^Error parsing YAML:")
                self.assertRegex(stderr, "Fix the YAML data\n$")


class CreateTests(CreateTestsBase):
    """Tests for the CLI :py:class:`workflow_template.Create` class."""

    def assertStdoutYaml(
        self,
        workspace: str,  # noqa: U100
        stdout: str,
        result: WorkflowTemplateData,
    ) -> None:
        """Ensure the program stdout matches the given result."""
        actual = WorkflowTemplateData.parse_obj(yaml.safe_load(stdout))
        self.assertEqual(actual, result)

    def command(
        self, task_name: str, template_name: str, *args: str
    ) -> list[str]:
        """Build a command line."""
        return ["workflow-template", "create", task_name, template_name, *args]


class LegacyCreateTests(CreateTestsBase):
    """Tests for the CLI :py:class:`workflow_template.LegacyCreate` class."""

    def assertStdoutYaml(
        self, workspace: str, stdout: str, result: WorkflowTemplateData
    ) -> None:
        """Ensure the program stdout matches the given result."""
        url = (
            f"https://debusine.debian.org/debian/{workspace}/"
            f"workflow-template/{result.name}/"
        )
        self.assertEqual(
            yaml.safe_load(stdout),
            {
                "workflow_template_id": result.id,
                "result": "success",
                "message": f"Workflow template registered: {url}",
            },
        )

    def command(
        self, task_name: str, template_name: str, *args: str
    ) -> list[str]:
        """Build a command line."""
        return ["create-workflow-template", template_name, task_name, *args]


del CreateTestsBase


class ListTests(BaseCliTests):
    """Tests for the :py:class:`workflow_templates.List` class."""

    def setUp(self) -> None:
        super().setUp()
        self.data = WorkflowTemplateData(
            id=42,
            name="sample",
            task_name="noop",
            task_data={"test": 2},
            priority=42,
        )

    def test_list(self) -> None:
        command = self.create_command(
            ["workflow-template", "list", "--workspace=workspace"]
        )
        assert isinstance(command, DebusineCommand)
        with (
            mock.patch.object(
                command.debusine,
                "workflow_template_iter",
                return_value=[self.data],
            ) as collection_iter,
            mock.patch.object(command, "list") as list_,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        collection_iter.assert_called_once_with("workspace")
        list_.assert_called_once_with([self.data])


class ShowTests(BaseCliTests):
    """Tests for the :py:class:`workflow_template.Show` class."""

    def setUp(self) -> None:
        super().setUp()
        self.data = WorkflowTemplateData(
            id=42,
            name="sample",
            task_name="noop",
            task_data={"test": 2},
            priority=42,
        )

    def test_show_id(self) -> None:
        command = self.create_command(
            ["workflow-template", "show", "--workspace=workspace", "42"]
        )
        assert isinstance(command, DebusineCommand)
        with (
            mock.patch.object(
                command.debusine,
                "workflow_template_get",
                return_value=self.data,
            ) as get,
            mock.patch.object(command, "show") as show,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        get.assert_called_once_with("workspace", "42")
        show.assert_called_once_with(self.data)

    def test_show_name(self) -> None:
        command = self.create_command(
            ["workflow-template", "show", "--workspace=workspace", "sample"]
        )
        assert isinstance(command, DebusineCommand)
        with (
            mock.patch.object(
                command.debusine,
                "workflow_template_get",
                return_value=self.data,
            ) as get,
            mock.patch.object(command, "show") as show,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        get.assert_called_once_with("workspace", "sample")
        show.assert_called_once_with(self.data)


class ManageTests(BaseCliTests):
    """Tests for the :py:class:`workflow_templates.Manage` class."""

    def setUp(self) -> None:
        """Set up the test case."""
        super().setUp()
        self.sample = WorkflowTemplateData(
            id=42,
            name="sample",
            task_name="noop",
            task_data={"test": 2},
            priority=42,
        )
        self.edited = self.sample.copy()

    def assertManages(self, *args: str) -> None:
        """Call workflow_template_update and match self.edited."""
        command = self.create_command(
            [
                "workflow-template",
                "manage",
                "--workspace=workspace",
                "sample",
                *args,
            ]
        )
        assert isinstance(command, DebusineCommand)

        with (
            mock.patch.object(
                command.debusine,
                "workflow_template_get",
                return_value=self.sample,
            ),
            mock.patch.object(
                command.debusine,
                "workflow_template_update",
                return_value=self.edited,
            ) as update,
            mock.patch.object(command, "show") as show,
        ):
            stderr, stdout = self.capture_output(command.run)

        self.assertEqual(stdout, "")
        self.assertEqual(stderr, "")
        update.assert_called_once_with("workspace", self.edited)
        show.assert_called_once_with(self.edited)

    def test_rename(self) -> None:
        self.edited.name = "renamed"
        self.assertManages("--rename=renamed")

    def test_set_priority(self) -> None:
        self.edited.priority = 1
        self.assertManages("--priority=1")

    def test_set_data(self) -> None:
        infile = self.create_temporary_file()
        infile.write_text("{'test': 7}")
        self.edited.task_data = {"test": 7}
        self.assertManages(f"--data={infile}")

    def test_set_data_stdin(self) -> None:
        self.enterContext(self.patch_sys_stdin_read("{'test': 7}"))
        self.edited.task_data = {"test": 7}
        self.assertManages("--data=-")
