# 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 Workflow commands."""

from unittest import mock
from unittest.mock import MagicMock
from urllib.parse import quote, urljoin

import yaml

from debusine.client.cli import Cli
from debusine.client.commands.tests.base import BaseCliTests
from debusine.client.debusine import Debusine
from debusine.client.exceptions import DebusineError
from debusine.client.models import CreateWorkflowRequest
from debusine.test.test_utils import create_work_request_response


class StartTests(BaseCliTests):
    """Tests for the CLI `workflow start` command."""

    def test_invalid_template_name(self) -> None:
        """CLI fails if the template name is bad."""
        template_name = "template-name"

        self.enterContext(self.patch_sys_stdin_read("{}"))
        cli = self.create_cli(["workflow", "start", template_name])

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

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

    def assert_create_workflow(
        self,
        cli: Cli,
        mocked_workflow_create: MagicMock,
        workflow_request: CreateWorkflowRequest,
        data_from_stdin: bool = False,
    ) -> None:
        """
        Call cli.execute() and assert the workflow template is created.

        Assert expected stderr/stdout and network call.

        :param data_from_stdin: if True, input data came from stdin.
        """
        stderr, stdout = self.capture_output(cli.execute)

        base_url = self.get_base_url(self.default_server)
        scope = self.servers[self.default_server]["scope"]
        workspace = workflow_request.workspace or "Testing"
        workflow_id = mocked_workflow_create.return_value.id
        workflow_url = urljoin(
            base_url,
            f"{quote(scope)}/{quote(workspace)}/work-request/{workflow_id}/",
        )

        expected = yaml.safe_dump(
            {
                "result": "success",
                "message": f"Workflow created: {workflow_url}",
                "workflow_id": workflow_id,
            },
            sort_keys=False,
        )
        if data_from_stdin:
            expected = f"---\n{expected}"
        self.assertEqual(stdout, expected)
        self.assertEqual(stderr, "")

        actual_workflow_request = mocked_workflow_create.mock_calls[0][1][1]

        # Verify CreateWorkflowRequest is as expected
        self.assertEqual(actual_workflow_request, workflow_request)

    def verify_create_workflow_scenario(
        self,
        cli_args: list[str],
        workspace: str = "developers",
        use_stdin: bool = False,
    ) -> None:
        """
        Test create-workflow using workspace/stdin/stdin-data.

        :param cli_args: first parameter is template_name.  'workflow start'
          is prepended to them.
        :param workspace: workspace to use, or None if not specified in the
          cli_args.
        :param use_stdin: to pass the generated data: use_stdin or a file.
        """
        template_name = cli_args[0]

        workflow_id = 11
        data = {"input": {"source_artifact_id": 1}}

        serialized_data = yaml.safe_dump(data)

        if use_stdin:
            self.enterContext(self.patch_sys_stdin_read(serialized_data))
            data_option = "-"
        else:
            data_file = self.create_temporary_file(
                contents=serialized_data.encode("utf-8")
            )
            data_option = str(data_file)

        cli = self.create_cli(
            ["workflow", "start"] + cli_args + (["--data", data_option])
        )

        expected_workflow_request = CreateWorkflowRequest(
            template_name=template_name, workspace=workspace, task_data=data
        )
        with mock.patch.object(
            Debusine,
            "workflow_create",
            autospec=True,
            return_value=create_work_request_response(
                base_url=self.get_base_url(self.default_server),
                scope=self.servers[self.default_server]["scope"],
                id=workflow_id,
                workspace=workspace,
            ),
        ) as mocked_workflow_create:
            self.assert_create_workflow(
                cli,
                mocked_workflow_create,
                expected_workflow_request,
                data_from_stdin=use_stdin,
            )

    def test_specific_workspace(self) -> None:
        """CLI parses the command line and uses --workspace."""
        workspace_name = "Testing"
        args = ["sbuild-amd64-arm64", "--workspace", workspace_name]
        self.verify_create_workflow_scenario(args, workspace=workspace_name)

    def test_success_data_from_file(self) -> None:
        """CLI creates a workflow with data from a file."""
        args = ["sbuild-amd64-arm64"]
        self.verify_create_workflow_scenario(args)

    def test_success_data_from_stdin(self) -> None:
        """CLI creates a workflow with data from stdin."""
        args = ["sbuild-amd64-arm64"]
        self.verify_create_workflow_scenario(args, use_stdin=True)

    def test_data_is_empty(self) -> None:
        """CLI creates a workflow with empty data."""
        empty_file = self.create_temporary_file()
        cli = self.create_cli(
            [
                "workflow",
                "start",
                "sbuild-amd64-arm64",
                "--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."""
        workflows = [
            {
                "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 in workflows:
            task_data = workflow["task_data"]
            with self.subTest(task_data), self.patch_sys_stdin_read(task_data):
                cli = self.create_cli(["workflow", "start", "workflow-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$")
