# 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 management command create_work_request."""

import io
from typing import Any

import yaml
from django.conf import settings
from django.core.management import CommandError

from debusine.artifacts.models import TaskTypes
from debusine.db.models import (
    User,
    WorkRequest,
    Workspace,
    default_workspace,
    system_user,
)
from debusine.django.management.tests import call_command
from debusine.tasks.models import ActionSendNotification, EventReactions
from debusine.test.django import TestCase


class CreateWorkRequestCommandTests(TestCase):
    """Tests for the create_work_request command."""

    def assert_work_request(
        self,
        work_request: WorkRequest,
        *,
        workspace: Workspace | None = None,
        created_by: User | None = None,
        task_data: dict[str, Any] | None = None,
        event_reactions: EventReactions | None = None,
    ) -> None:
        self.assertEqual(
            work_request.workspace, workspace or default_workspace()
        )
        self.assertEqual(work_request.created_by, created_by or system_user())
        self.assertEqual(work_request.status, WorkRequest.Statuses.PENDING)
        self.assertEqual(work_request.task_data, task_data or {})
        self.assertEqual(
            work_request.event_reactions, event_reactions or EventReactions()
        )

    def test_data_from_file(self) -> None:
        """`create_work_request` accepts data from a file."""
        data = {"result": WorkRequest.Results.SUCCESS}
        data_file = self.create_temporary_file(
            contents=yaml.safe_dump(data).encode()
        )
        stdout, stderr, exit_code = call_command(
            "create_work_request",
            "worker",
            "noop",
            "--data",
            str(data_file),
        )

        work_request = WorkRequest.objects.get(
            task_type=TaskTypes.WORKER, task_name="noop"
        )
        self.assert_work_request(work_request, task_data=data)

        expected = yaml.safe_dump({"work_request_id": work_request.id})
        self.assertEqual(stdout, f"---\n{expected}\n")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

    def test_data_from_stdin(self) -> None:
        """`create_work_request` accepts data from stdin."""
        data = {"result": WorkRequest.Results.SUCCESS}
        stdout, stderr, exit_code = call_command(
            "create_work_request",
            "worker",
            "noop",
            stdin=io.StringIO(yaml.safe_dump(data)),
        )

        work_request = WorkRequest.objects.get(
            task_type=TaskTypes.WORKER, task_name="noop"
        )
        self.assert_work_request(work_request, task_data=data)

        expected = yaml.safe_dump({"work_request_id": work_request.id})
        self.assertEqual(stdout, f"---\n{expected}\n")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

    def test_event_reactions(self) -> None:
        """`create_work_request` handles event reactions."""
        data = {"result": WorkRequest.Results.SUCCESS}
        event_reactions = {
            "on_success": [{"action": "send-notification", "channel": "foo"}]
        }
        event_reactions_file = self.create_temporary_file(
            contents=yaml.safe_dump(event_reactions).encode()
        )
        stdout, stderr, exit_code = call_command(
            "create_work_request",
            "worker",
            "noop",
            "--event-reactions",
            str(event_reactions_file),
            stdin=io.StringIO(yaml.safe_dump(data)),
        )

        work_request = WorkRequest.objects.get(
            task_type=TaskTypes.WORKER, task_name="noop"
        )
        self.assert_work_request(
            work_request,
            task_data=data,
            event_reactions=EventReactions(
                on_success=[ActionSendNotification(channel="foo")]
            ),
        )

        expected = yaml.safe_dump({"work_request_id": work_request.id})
        self.assertEqual(stdout, f"---\n{expected}\n")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

    def test_server(self) -> None:
        data = {"result": WorkRequest.Results.SUCCESS}
        stdout, stderr, exit_code = call_command(
            "create_work_request",
            "server",
            "servernoop",
            stdin=io.StringIO(yaml.safe_dump(data)),
        )

        work_request = WorkRequest.objects.get(
            task_type=TaskTypes.SERVER, task_name="servernoop"
        )
        self.assert_work_request(work_request, task_data=data)

        expected = yaml.safe_dump({"work_request_id": work_request.id})
        self.assertEqual(stdout, f"---\n{expected}\n")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

    def test_server_no_event_reactions(self) -> None:
        data = {"result": WorkRequest.Results.SUCCESS}
        event_reactions = {
            "on_success": [
                {
                    "action": "update-collection-with-artifacts",
                    "collection": 123,
                    "artifact_filters": {"category": "test"},
                }
            ]
        }
        event_reactions_file = self.create_temporary_file(
            contents=yaml.safe_dump(event_reactions).encode()
        )
        with self.assertRaisesRegex(
            CommandError, r"^Event reactions are not supported for SERVER tasks"
        ) as exc:
            call_command(
                "create_work_request",
                "server",
                "servernoop",
                "--event-reactions",
                str(event_reactions_file),
                stdin=io.StringIO(yaml.safe_dump(data)),
            )
        self.assertEqual(exc.exception.returncode, 1)

    def test_signing(self) -> None:
        """`create_work_request` can create signing work requests."""
        data = {"purpose": "uefi", "description": "A UEFI key"}
        stdout, stderr, exit_code = call_command(
            "create_work_request",
            "signing",
            "generatekey",
            stdin=io.StringIO(yaml.safe_dump(data)),
        )

        work_request = WorkRequest.objects.get(
            task_type=TaskTypes.SIGNING, task_name="generatekey"
        )
        self.assert_work_request(work_request, task_data=data)

        expected = yaml.safe_dump({"work_request_id": work_request.id})
        self.assertEqual(stdout, f"---\n{expected}\n")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

    def test_signing_no_event_reactions(self) -> None:
        data = {"purpose": "uefi", "description": "A UEFI key"}
        event_reactions = {
            "on_success": [
                {
                    "action": "update-collection-with-artifacts",
                    "collection": 123,
                    "artifact_filters": {"category": "test"},
                }
            ]
        }
        event_reactions_file = self.create_temporary_file(
            contents=yaml.safe_dump(event_reactions).encode()
        )
        with self.assertRaisesRegex(
            CommandError,
            r"^Event reactions are not supported for SIGNING tasks",
        ) as exc:
            call_command(
                "create_work_request",
                "signing",
                "generatekey",
                "--event-reactions",
                str(event_reactions_file),
                stdin=io.StringIO(yaml.safe_dump(data)),
            )
        self.assertEqual(exc.exception.returncode, 1)

    def test_wait_not_implemented(self) -> None:
        """`create_work_request` refuses to create wait work requests."""
        data = {"result": WorkRequest.Results.SUCCESS}
        with self.assertRaisesRegex(
            CommandError, r"^Error: argument task_type: invalid choice: 'wait'"
        ) as exc:
            call_command(
                "create_work_request",
                "wait",
                "externaldebsign",
                stdin=io.StringIO(yaml.safe_dump(data)),
            )
        self.assertEqual(exc.exception.returncode, 1)

        self.assertQuerySetEqual(
            WorkRequest.objects.filter(
                task_type=TaskTypes.WAIT, task_name="externaldebsign"
            ),
            [],
        )

    def test_empty_data(self) -> None:
        """`create_work_request` defaults data to {}."""
        stdout, stderr, exit_code = call_command(
            "create_work_request", "worker", "noop", stdin=io.StringIO()
        )

        work_request = WorkRequest.objects.get(
            task_type=TaskTypes.WORKER, task_name="noop"
        )
        self.assert_work_request(work_request, task_data={})

        expected = yaml.safe_dump({"work_request_id": work_request.id})
        self.assertEqual(stdout, f"---\n{expected}\n")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

    def test_different_created_by(self) -> None:
        """`create_work_request` can use a different created-by user."""
        user = self.playground.get_default_user()
        data = {"result": WorkRequest.Results.SUCCESS}
        stdout, stderr, exit_code = call_command(
            "create_work_request",
            "worker",
            "noop",
            "--created-by",
            user.username,
            stdin=io.StringIO(yaml.safe_dump(data)),
        )

        work_request = WorkRequest.objects.get(
            task_type=TaskTypes.WORKER, task_name="noop"
        )
        self.assert_work_request(work_request, created_by=user, task_data=data)

        expected = yaml.safe_dump({"work_request_id": work_request.id})
        self.assertEqual(stdout, f"---\n{expected}\n")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

    def test_different_workspace(self) -> None:
        """`create_work_request` can use a non-default workspace."""
        workspace_name = "test-workspace"
        workspace = self.playground.create_workspace(name=workspace_name)
        stdout, stderr, exit_code = call_command(
            "create_work_request",
            "worker",
            "noop",
            "--workspace",
            workspace_name,
            stdin=io.StringIO(),
        )

        work_request = WorkRequest.objects.get(
            task_type=TaskTypes.WORKER, task_name="noop"
        )
        self.assert_work_request(work_request, workspace=workspace)

        expected = yaml.safe_dump({"work_request_id": work_request.id})
        self.assertEqual(stdout, f"---\n{expected}\n")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

    def test_different_scope(self) -> None:
        """`create_work_request` can use a workspace in a different scope."""
        scope_name = "test-scope"
        scope = self.playground.get_or_create_scope(name=scope_name)
        workspace = self.playground.create_workspace(
            scope=scope, name=settings.DEBUSINE_DEFAULT_WORKSPACE
        )
        stdout, stderr, exit_code = call_command(
            "create_work_request",
            "worker",
            "noop",
            "--workspace",
            f"{scope_name}/{settings.DEBUSINE_DEFAULT_WORKSPACE}",
            stdin=io.StringIO(),
        )

        work_request = WorkRequest.objects.get(
            task_type=TaskTypes.WORKER, task_name="noop"
        )
        self.assert_work_request(work_request, workspace=workspace)

        expected = yaml.safe_dump({"work_request_id": work_request.id})
        self.assertEqual(stdout, f"---\n{expected}\n")
        self.assertEqual(stderr, "")
        self.assertEqual(exit_code, 0)

    def test_invalid_data_yaml(self) -> None:
        """`create_work_request` returns error: cannot parse data."""
        with self.assertRaisesRegex(
            CommandError, r"^Error parsing YAML:"
        ) as exc:
            call_command(
                "create_work_request", "worker", "noop", stdin=io.StringIO(":")
            )

        self.assertEqual(exc.exception.returncode, 3)

    def test_invalid_event_reactions_yaml(self) -> None:
        """`create_work_request` returns error: cannot parse data."""
        event_reactions_file = self.create_temporary_file(contents=b":")

        with self.assertRaisesRegex(
            CommandError, r"^Error parsing YAML:"
        ) as exc:
            call_command(
                "create_work_request",
                "worker",
                "noop",
                "--event-reactions",
                str(event_reactions_file),
                stdin=io.StringIO(),
            )

        self.assertEqual(exc.exception.returncode, 3)

    def test_bad_task_type(self) -> None:
        """`create_work_request` returns error: bad task type."""
        with self.assertRaisesRegex(
            CommandError, r"task_type: invalid choice: 'nonexistent'"
        ) as exc:
            call_command(
                "create_work_request",
                "nonexistent",
                "noop",
                stdin=io.StringIO(),
            )

        self.assertEqual(exc.exception.returncode, 1)

    def test_user_not_found(self) -> None:
        """`create_work_request` returns error: user not found."""
        with self.assertRaisesRegex(
            CommandError, r'^User "nonexistent" not found'
        ) as exc:
            call_command(
                "create_work_request",
                "worker",
                "noop",
                "--created-by",
                "nonexistent",
                stdin=io.StringIO(),
            )

        self.assertEqual(exc.exception.returncode, 3)

    def test_workspace_not_found(self) -> None:
        """`create_work_request` returns error: workspace not found."""
        with self.assertRaisesRegex(
            CommandError, r"^Workspace 'nonexistent' not found"
        ) as exc:
            call_command(
                "create_work_request",
                "worker",
                "noop",
                "--workspace",
                "nonexistent",
                stdin=io.StringIO(),
            )

        self.assertEqual(exc.exception.returncode, 3)

    def test_task_name_not_found(self) -> None:
        """`create_work_request` returns error: task name not found."""
        with self.assertRaisesRegex(
            CommandError,
            r"'task_name': \['nonexistent: invalid Worker task name'\]",
        ) as exc:
            call_command(
                "create_work_request",
                "worker",
                "nonexistent",
                stdin=io.StringIO(),
            )

        self.assertEqual(exc.exception.returncode, 3)

    def test_bad_task_data(self) -> None:
        """`create_work_request` returns error: bad task data."""
        with self.assertRaisesRegex(
            CommandError, r"invalid Worker task data"
        ) as exc:
            call_command(
                "create_work_request",
                "worker",
                "noop",
                stdin=io.StringIO("foo: bar\n"),
            )

        self.assertEqual(exc.exception.returncode, 3)
        self.assertFalse(
            WorkRequest.objects.filter(
                task_type=TaskTypes.WORKER, task_name="noop"
            ).exists()
        )
