# 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 Debusine CLI suite management commands."""

from typing import Any
from unittest import mock

import yaml

from debusine.artifacts.models import CollectionCategory
from debusine.client.commands.archives.suites import Create
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 (
    CollectionData,
    CollectionDataNew,
    CollectionRelationType,
    LookupChildType,
    LookupResultType,
    LookupSingleResponse,
    RuntimeParameter,
    WorkflowTemplateData,
    WorkflowTemplateDataNew,
    WorkspaceInheritanceChain,
    WorkspaceInheritanceChainElement,
)


class CreateTests(BaseCliTests):
    """Tests for ``debusine archive suite create``."""

    def cmdline(self, *args: str) -> list[str]:
        """Shortcut to build a command."""
        return ["archive", "suite", "create", *args]

    def chain(
        self,
        *args: WorkspaceInheritanceChainElement,
    ) -> WorkspaceInheritanceChain:
        """Shortcut to build an inheritance chain."""
        return WorkspaceInheritanceChain(chain=list(args))

    def suite(self, name: str, data: dict[str, Any]) -> CollectionData:
        """Shortcut to build a suite collection."""
        return CollectionData(
            id=1,
            name=name,
            category=CollectionCategory.SUITE,
            data=data,
            links={"webui_self": "/c/1"},
        )

    def workflow_template(
        self,
        name: str,
        task_name: str,
        workflow_template_id: int = 1,
        static_parameters: dict[str, Any] | None = None,
    ) -> WorkflowTemplateData:
        """Shortcut to build a workflow template."""
        return WorkflowTemplateData(
            id=workflow_template_id,
            name=name,
            task_name=task_name,
            static_parameters=static_parameters or {},
            runtime_parameters=RuntimeParameter.ANY,
            links={"webui_self": f"/wt/{workflow_template_id}"},
        )

    def test_base_workflow_template_nonexistent_workspace(self) -> None:
        command = self.create_command(
            self.cmdline(
                "--workspace=nonexistent",
                "--architecture=amd64",
                "--base-workflow-template=upload-to-unstable",
                "sid-overlay",
            )
        )
        assert isinstance(command, Create)

        with (
            mock.patch.object(
                Debusine,
                "workflow_template_get",
                side_effect=DebusineError(title="Workspace not found"),
            ),
            mock.patch.object(
                Debusine,
                "get_workspace_inheritance",
                side_effect=DebusineError(title="Workspace not found"),
            ),
            self.assertRaises(DebusineError) as raised,
        ):
            self.capture_output(
                command.get_base_workflow_template, assert_system_exit_code=3
            )

        self.assertDebusineError(
            raised.exception, {"title": "Workspace not found"}
        )

    def test_base_workflow_template_in_different_scope(self) -> None:
        command = self.create_command(
            self.cmdline(
                "--workspace=workspace",
                "--architecture=amd64",
                "--base-workflow-template=upload-to-unstable",
                "sid-overlay",
            )
        )
        assert isinstance(command, Create)
        El = WorkspaceInheritanceChainElement
        chain = self.chain(El(scope="other-scope", workspace="base"))

        with (
            mock.patch.object(
                Debusine,
                "workflow_template_get",
                side_effect=DebusineError(title="Object not found"),
            ),
            mock.patch.object(
                Debusine, "get_workspace_inheritance", return_value=chain
            ),
        ):
            stderr, _ = self.capture_output(
                command.get_base_workflow_template, assert_system_exit_code=3
            )

        self.assertEqual(
            stderr,
            "Workflow template 'upload-to-unstable' not found in any parent "
            "workspace in this scope.\n",
        )

    def test_base_workflow_template_wrong_task_name(self) -> None:
        command = self.create_command(
            self.cmdline(
                "--workspace=workspace",
                "--architecture=amd64",
                "--base-workflow-template=sample",
                "sid-overlay",
            )
        )
        assert isinstance(command, Create)

        with mock.patch.object(
            Debusine,
            "workflow_template_get",
            return_value=self.workflow_template("sample", "noop"),
        ):
            stderr, _ = self.capture_output(
                command.get_base_workflow_template, assert_system_exit_code=3
            )

        self.assertEqual(
            stderr,
            "Workflow template 'sample' is not a debian_pipeline template.\n",
        )

    def test_base_workflow_template_in_current_workspace(self) -> None:
        command = self.create_command(
            self.cmdline(
                "--architecture=amd64",
                "--base-workflow-template=upload-to-unstable",
                "sid-overlay",
            )
        )
        assert isinstance(command, Create)
        base_workflow_template = self.workflow_template(
            "upload-to-unstable", "debian_pipeline"
        )

        with mock.patch.object(
            Debusine,
            "workflow_template_get",
            return_value=base_workflow_template,
        ) as workflow_template_get:
            self.assertEqual(
                command.get_base_workflow_template(), base_workflow_template
            )

        workflow_template_get.assert_called_once_with(
            "developers", "upload-to-unstable"
        )

    def test_base_workflow_template_in_parent_workspace(self) -> None:
        command = self.create_command(
            self.cmdline(
                "--workspace=workspace",
                "--architecture=amd64",
                "--base-workflow-template=upload-to-unstable",
                "sid-overlay",
            )
        )
        assert isinstance(command, Create)
        El = WorkspaceInheritanceChainElement
        chain = self.chain(El(scope=self.default_server, workspace="base"))
        base_workflow_template = self.workflow_template(
            "upload-to-unstable", "debian_pipeline"
        )

        with (
            mock.patch.object(
                Debusine,
                "workflow_template_get",
                side_effect=iter(
                    [
                        DebusineError(title="Object not found"),
                        base_workflow_template,
                    ]
                ),
            ) as workflow_template_get,
            mock.patch.object(
                Debusine, "get_workspace_inheritance", return_value=chain
            ),
        ):
            self.assertEqual(
                command.get_base_workflow_template(), base_workflow_template
            )

        self.assertEqual(workflow_template_get.call_count, 2)
        workflow_template_get.assert_has_calls(
            [
                mock.call("workspace", "upload-to-unstable"),
                mock.call("base", "upload-to-unstable"),
            ]
        )

    def test_adjust_static_parameters_with_architecture_all(self) -> None:
        command = self.create_command(
            self.cmdline(
                "--workspace=workspace",
                "--architecture=all",
                "--architecture=amd64",
                "--base-workflow-template=upload-to-unstable",
                "sid-overlay",
            )
        )
        assert isinstance(command, Create)

        self.assertEqual(
            command.adjust_static_parameters(
                {"vendor": "debian", "enable_upload": True}
            ),
            {
                "vendor": "debian",
                "suite": f"sid-overlay@{CollectionCategory.SUITE}",
                "architectures_allowlist": ["all", "amd64"],
            },
        )

    def test_adjust_static_parameters_without_architecture_all(self) -> None:
        command = self.create_command(
            self.cmdline(
                "--workspace=workspace",
                "--architecture=amd64",
                "--architecture=i386",
                "--base-workflow-template=upload-to-unstable",
                "sid-overlay",
            )
        )
        assert isinstance(command, Create)

        self.assertEqual(
            command.adjust_static_parameters(
                {"vendor": "debian", "enable_upload": True}
            ),
            {
                "vendor": "debian",
                "suite": f"sid-overlay@{CollectionCategory.SUITE}",
                "architectures_allowlist": ["all", "amd64", "i386"],
            },
        )

    def test_run_create_collection_error(self) -> None:
        cli = self.create_cli(
            self.cmdline(
                "--workspace=workspace", "--architecture=amd64", "sid-overlay"
            )
        )

        with mock.patch.object(
            Debusine,
            "collection_create",
            side_effect=DebusineError(title="expected error"),
        ):
            exception = self.assertShowsError(cli.execute)

        self.assertDebusineError(exception, {"title": "expected error"})

    def test_run_create_workflow_template_error(self) -> None:
        cli = self.create_cli(
            self.cmdline(
                "--workspace=workspace",
                "--architecture=amd64",
                "--base-workflow-template=upload-to-unstable",
                "sid-overlay",
            )
        )
        base_workflow_template = self.workflow_template(
            "upload-to-unstable", "debian_pipeline"
        )
        suite = self.suite(
            "sid-overlay", {"components": "main", "architectures": ["amd64"]}
        )

        with (
            mock.patch.object(
                Debusine,
                "workflow_template_get",
                return_value=base_workflow_template,
            ),
            mock.patch.object(
                Debusine, "collection_create", return_value=suite
            ),
            mock.patch.object(
                Debusine,
                "workflow_template_create",
                side_effect=DebusineError(title="expected error"),
            ),
        ):
            exception = self.assertShowsError(cli.execute)

        self.assertDebusineError(exception, {"title": "expected error"})

    def test_run_without_base_workflow_template(self) -> None:
        cli = self.create_cli(
            self.cmdline(
                "--workspace=workspace",
                "--architecture=amd64",
                "--architecture=i386",
                "sid-overlay",
            )
        )
        expected_suite_data = {
            "components": ["main"],
            "architectures": ["amd64", "i386"],
        }
        suite = self.suite("sid-overlay", expected_suite_data)

        with mock.patch.object(
            Debusine, "collection_create", return_value=suite
        ) as collection_create:
            stderr, stdout = self.capture_output(cli.execute)

        collection_create.assert_called_once_with(
            "workspace",
            CollectionDataNew(
                name="sid-overlay",
                category=CollectionCategory.SUITE,
                data=expected_suite_data,
            ),
        )
        self.assertEqual(stderr, "")
        self.assertEqual(
            yaml.safe_load(stdout),
            {"result": "success", "suite": "https://debusine.debian.org/c/1"},
        )

    def assert_run_with_base_workflow_template(
        self,
        *,
        qa_suite: str | None = None,
        regression_tracking_qa_results: str | None = None,
    ) -> None:
        assert not (qa_suite and regression_tracking_qa_results), (
            "Only one can be tested at a time"
        )

        cli = self.create_cli(
            self.cmdline(
                "--workspace=workspace",
                "--architecture=amd64",
                "--architecture=i386",
                "--base-workflow-template=upload-to-unstable",
                "sid-overlay",
            )
        )
        expected_suite_data = {
            "components": ["main"],
            "architectures": ["amd64", "i386"],
        }
        suite = self.suite("sid-overlay", expected_suite_data)

        relation_type = None
        relations_parameters = {}
        if qa_suite is not None:
            relations_parameters["qa_suite"] = qa_suite
            relation_type = CollectionRelationType.SUITE_TARGETING
        elif regression_tracking_qa_results is not None:
            relations_parameters["regression_tracking_qa_results"] = (
                regression_tracking_qa_results
            )
            relation_type = CollectionRelationType.SUITE_DEFAULT_QA_RESULTS

        expected_static_parameters = {
            "vendor": "debian",
            "suite": f"sid-overlay@{CollectionCategory.SUITE}",
            "architectures_allowlist": ["all", "amd64", "i386"],
            **relations_parameters,
        }

        base_workflow_template = self.workflow_template(
            "upload-to-unstable",
            "debian_pipeline",
            static_parameters={
                "vendor": "debian",
                "enable_upload": True,
                **relations_parameters,
            },
        )

        workflow_template = self.workflow_template(
            "publish-to-sid-overlay",
            "debian_pipeline",
            workflow_template_id=2,
            static_parameters=expected_static_parameters,
        )
        collection_id = 209
        name_resolved_collection = "trixie-proposed-updates"
        with (
            mock.patch.object(
                Debusine,
                "workflow_template_get",
                return_value=base_workflow_template,
            ),
            mock.patch.object(
                Debusine, "collection_create", return_value=suite
            ) as collection_create,
            mock.patch.object(
                Debusine,
                "workflow_template_create",
                return_value=workflow_template,
            ) as workflow_template_create,
            mock.patch.object(
                Debusine,
                "lookup_single",
                return_value=LookupSingleResponse(
                    result_type=LookupResultType.COLLECTION,
                    collection=collection_id,
                ),
            ) as lookup_single,
            mock.patch.object(
                Debusine,
                "collection_get_by_id",
                return_value=CollectionData(
                    id=collection_id,
                    name=name_resolved_collection,
                    category=CollectionCategory.SUITE,
                    data={},
                ),
            ) as collection_get_by_id,
            mock.patch.object(
                Create,
                "_set_suite_relations",
            ) as set_suite_relations,
        ):
            stderr, stdout = self.capture_output(cli.execute)

        collection_create.assert_called_once_with(
            "workspace",
            CollectionDataNew(
                name="sid-overlay",
                category=CollectionCategory.SUITE,
                data=expected_suite_data,
            ),
        )

        workflow_template_create.assert_called_once_with(
            "workspace",
            WorkflowTemplateDataNew(
                name="publish-to-sid-overlay",
                task_name="debian_pipeline",
                static_parameters=expected_static_parameters,
                runtime_parameters=RuntimeParameter.ANY,
                priority=0,
            ),
        )
        if qa_suite is None and regression_tracking_qa_results is None:
            set_suite_relations.assert_not_called()
        else:
            lookup_single.assert_called_once_with(
                lookup=qa_suite or regression_tracking_qa_results,
                workspace="workspace",
                expect_type=LookupChildType.COLLECTION,
                default_category=CollectionCategory.SUITE,
            )
            collection_get_by_id.assert_called_once_with(
                workspace="workspace", collection_id=collection_id
            )

            set_suite_relations.assert_called_once_with(
                relation_type,
                [f"{name_resolved_collection}@{CollectionCategory.SUITE}"],
            )

        self.assertEqual(stderr, "")
        output_expected: dict[str, Any] = {
            "result": "success",
            "suite": "https://debusine.debian.org/c/1",
            "workflow_template": "https://debusine.debian.org/wt/2",
        }

        relations = None
        if qa_suite is not None:
            relations = {
                "suite-targeting": (
                    f"{name_resolved_collection}@{CollectionCategory.SUITE}"
                )
            }
        elif regression_tracking_qa_results is not None:
            relations = {
                "suite-default-qa-results": (
                    f"{name_resolved_collection}@{CollectionCategory.SUITE}"
                )
            }

        if relations is not None:
            output_expected["relations"] = relations

        self.assertEqual(yaml.safe_load(stdout), output_expected)

    def test_run_with_base_workflow_template(self) -> None:
        self.assert_run_with_base_workflow_template()

    def test_run_with_base_workflow_template_qa_suite(self) -> None:
        self.assert_run_with_base_workflow_template(qa_suite="25")

    def test_run_with_base_workflow_template_regression_tracking(self) -> None:
        self.assert_run_with_base_workflow_template(
            regression_tracking_qa_results="25"
        )

    def test_set_suite_relations(self) -> None:
        command = self.create_command(
            self.cmdline(
                "--workspace=Debian",
                "--architecture=amd64",
                "new-suite",
            )
        )
        assert isinstance(command, Create)

        targets = [f"trixie@{CollectionCategory.SUITE}"]

        with mock.patch.object(
            Debusine,
            "collection_relation_set",
        ) as collection_relation_set:
            command._set_suite_relations(
                CollectionRelationType.SUITE_TARGETING, targets
            )

        source = f"new-suite@{CollectionCategory.SUITE}"
        collection_relation_set.assert_called_once_with(
            "Debian",
            source=source,
            relation_type=CollectionRelationType.SUITE_TARGETING,
            targets=targets,
        )

    def test_run_with_base_workflow_template_qa_suite_lookup_error(
        self,
    ) -> None:
        cli = self.create_cli(
            self.cmdline(
                "--workspace=workspace",
                "--architecture=amd64",
                "--base-workflow-template=upload-to-unstable",
                "sid-overlay",
            )
        )

        base_workflow_template = self.workflow_template(
            "upload-to-unstable",
            "debian_pipeline",
            static_parameters={
                "vendor": "debian",
                "qa_suite": "something-does-not-exist@debian:suite",
            },
        )

        with (
            mock.patch.object(
                Debusine,
                "workflow_template_get",
                return_value=base_workflow_template,
            ),
            mock.patch.object(
                Debusine,
                "collection_create",
                return_value=self.suite(
                    "sid-overlay",
                    {"components": ["main"], "architectures": ["amd64"]},
                ),
            ),
            mock.patch.object(
                Debusine,
                "lookup_single",
                side_effect=DebusineError("lookup failed"),
            ),
        ):
            stderr, stdout = self.capture_output(
                cli.execute, assert_system_exit_code=3
            )

        self.assertEqual(
            stdout.replace("\n", ""),
            "Error: Cannot find collection in "
            "'something-does-not-exist@debian:suite' "
            "(from 'qa_suite' field in the base template)",
        )
        self.assertEqual(stderr, "")
