# 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 that try exploiting the system via its public API."""

from typing import Any, ClassVar

from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient

from debusine.assets import KeyPurpose as ArtifactKeyPurpose
from debusine.assets import SigningKeyData
from debusine.db.models import (
    Asset,
    AssetUsage,
    Token,
    WorkRequest,
    WorkflowTemplate,
    Workspace,
)
from debusine.db.playground import scenarios
from debusine.server.celery import run_server_task
from debusine.server.workflows.base import orchestrate_workflow
from debusine.signing.tasks.models import SignData, SignDynamicData
from debusine.tasks.models import LookupMultiple, WorkerType
from debusine.test.django import TestCase, TestResponseType


class PenetrationTests(TestCase):
    """Try to game the system using its API."""

    scenario = scenarios.DefaultContextAPI()

    create_child_workspace: ClassVar[WorkflowTemplate]
    signing_key: ClassVar[Asset]
    signing_key_usage: ClassVar[AssetUsage]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up common test data."""
        super().setUpTestData()
        # Allow creation of child workspaces
        cls.create_child_workspace = cls.playground.create_workflow_template(
            "create_child", "create_child_workspace"
        )
        cls.signing_key = cls.playground.create_signing_key_asset()
        cls.signing_key_usage = cls.playground.create_asset_usage(
            cls.signing_key
        )

    def setUp(self) -> None:
        """Set up common objects."""
        super().setUp()
        self.client = APIClient()

    def _post(
        self,
        endpoint: str,
        *,
        data: dict[str, Any],
        endpoint_kwargs: dict[str, Any] | None = None,
        scope: str,
        token: Token,
    ) -> TestResponseType:
        headers = {"Token": token.key}
        headers["X-Debusine-Scope"] = scope

        return self.client.post(
            reverse(endpoint, kwargs=endpoint_kwargs),
            data=data,
            headers=headers,
            format="json",
        )

    def post_workflow(self, data: dict[str, Any]) -> TestResponseType:
        """Post a workflow creation request to api:workflows."""
        return self._post(
            "api:workflows",
            data=data,
            scope=self.scenario.scope.name,
            token=self.scenario.user_token,
        )

    def post_permission_check(
        self,
        *,
        asset_category: str,
        asset_slug: str,
        permission_name: str,
        data: dict[str, Any],
        work_request: WorkRequest,
    ) -> TestResponseType:
        """Post an asset permission check request."""
        work_request.worker = self.playground.create_worker(
            worker_type=WorkerType.SIGNING
        )
        work_request.save()
        worker_token = self.playground.create_work_request_token(work_request)
        return self._post(
            "api:asset-permission-check",
            endpoint_kwargs={
                "asset_category": asset_category,
                "asset_slug": asset_slug,
                "permission_name": permission_name,
            },
            data=data,
            scope=self.scenario.scope.name,
            token=worker_token,
        )

    def assertChildWorkspaceCreated(self, **kwargs: Any) -> Workspace:
        """Create a child workspace and return its name."""
        response = self.post_workflow(
            {
                "template_name": "create_child",
                "task_data": kwargs,
            }
        )
        result = self.assertAPIResponseOk(response, status.HTTP_201_CREATED)

        # Run the workflow
        workflow = WorkRequest.objects.get(pk=result["id"])
        orchestrate_workflow(workflow)

        # Run the server task
        server_task = workflow.children.get()
        self.playground.advance_work_request(
            server_task, assign_new_worker=True
        )
        run_server_task.apply(args=(server_task.pk,))
        server_task.refresh_from_db()
        self.assertEqual(server_task.status, WorkRequest.Statuses.COMPLETED)
        self.assertEqual(server_task.result, WorkRequest.Results.SUCCESS)

        return Workspace.objects.get(
            scope=self.scenario.scope,
            name=f"{self.scenario.workspace.name}-test",
        )

    def test_use_signing_key_from_child(self) -> None:
        """Attempt to use a workflow signing key in a child workspace."""
        self.playground.create_group_role(
            self.scenario.workspace,
            Workspace.Roles.CONTRIBUTOR,
            users=[self.scenario.user],
        )
        new_workspace = self.assertChildWorkspaceCreated(suffix="test")
        signing_input = self.playground.create_signing_input_artifact(
            workspace=new_workspace
        )
        signing_key_data = self.signing_key.data_model
        assert isinstance(signing_key_data, SigningKeyData)

        signing_task = self.playground.create_signing_task(
            workspace=new_workspace,
            task_name="sign",
            task_data=SignData(
                purpose=ArtifactKeyPurpose(signing_key_data.purpose),
                unsigned=LookupMultiple((signing_input.id,)),
                key=signing_key_data.fingerprint,
            ),
            created_by=self.scenario.user,
            mark_running=True,
        )
        signing_task.dynamic_task_data = SignDynamicData(
            unsigned_ids=[signing_input.id],
            unsigned_binary_package_names=[
                signing_input.data["binary_package_name"]
            ],
        ).model_dump()
        signing_task.save()
        response = self.post_permission_check(
            asset_category=self.signing_key.category,
            asset_slug=self.signing_key.slug,
            permission_name="sign_with",
            data={
                "artifact_id": signing_input.id,
                "work_request_id": signing_task.id,
                "workspace": new_workspace.name,
            },
            work_request=signing_task,
        )
        result = self.assertAPIResponseOk(response)
        self.assertFalse(result["has_permission"])
