# Copyright 2024 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 database playground."""

import django.test
from django.contrib.auth.hashers import check_password

from debusine.artifacts.models import (
    ArtifactCategory,
    CollectionCategory,
    DebianPackageBuildLog,
    DebianSourcePackage,
)
from debusine.db.models import (
    Artifact,
    ArtifactRelation,
    DEFAULT_FILE_STORE_NAME,
    DEFAULT_WORKSPACE_NAME,
    FileInArtifact,
    FileStore,
    User,
    WorkRequest,
)
from debusine.server.workflows.models import WorkRequestWorkflowData
from debusine.tasks.models import (
    BackendType,
    SbuildBuildComponent,
    SbuildDynamicData,
    TaskTypes,
)
from debusine.test import TestCase
from debusine.test.playground import Playground


class PlaygroundTest(django.test.TestCase, TestCase):
    """Test playground functions."""

    def assert_artifact_relations(
        self,
        artifact: Artifact,
        targets: list[tuple[Artifact, ArtifactRelation.Relations]],
    ) -> None:
        """Check that an artifact has the given set of relations."""
        actual: list[tuple[Artifact, ArtifactRelation.Relations]] = []
        for relation in ArtifactRelation.objects.filter(artifact=artifact):
            actual.append(
                (relation.target, ArtifactRelation.Relations(relation.type))
            )
        self.assertEqual(
            sorted(actual, key=lambda x: (x[0].pk, x[1])),
            sorted(targets, key=lambda x: (x[0].pk, x[1])),
        )

    def test_defaults(self) -> None:
        """Check default playground configuration."""
        playground = Playground()
        user = playground.get_default_user()
        self.assertEqual(user.username, "playground")

        file_store = playground.get_default_file_store()
        self.assertEqual(file_store.name, DEFAULT_FILE_STORE_NAME)
        self.assertEqual(file_store.backend, FileStore.BackendChoices.MEMORY)

        workspace = playground.get_default_workspace()
        self.assertEqual(workspace.default_file_store, file_store)
        self.assertEqual(workspace.name, DEFAULT_WORKSPACE_NAME)

    def test_user_password(self) -> None:
        """Check that the default user password is set when requested."""
        playground = Playground(
            default_username="test1", default_user_email="test1@example.org"
        )
        user = playground.get_default_user()
        self.assertEqual(user.username, "test1")
        self.assertFalse(user.has_usable_password())

        playground = Playground(
            default_username="test2",
            default_user_password="test",
            default_user_email="test2@example.org",
        )
        user = playground.get_default_user()
        self.assertEqual(user.username, "test2")
        self.assertTrue(user.has_usable_password())
        self.assertTrue(check_password("test", user.password))

    def test_create_workspace_scope(self) -> None:
        """Test setting scope on created workspaces."""
        playground = Playground()
        testscope = playground.get_or_create_scope("testscope")

        ws1 = playground.create_workspace()
        self.assertEqual(playground.create_workspace(), ws1)

        ws2 = playground.create_workspace(scope=testscope)
        self.assertEqual(playground.create_workspace(scope=testscope), ws2)
        self.assertNotEqual(ws1, ws2)

    def test_create_workspace_file_store(self) -> None:
        """Test setting file store on created workspaces."""
        playground = Playground()
        ws1 = playground.create_workspace()
        self.assertEqual(
            ws1.default_file_store, playground.get_default_file_store()
        )

        custom_store = FileStore.objects.create(
            name="external",
            backend=FileStore.BackendChoices.EXTERNAL_DEBIAN_SUITE,
            configuration={
                "archive_root_url": "https://deb.debian.org/debian",
                "suite": "bookworm",
                "components": ["main"],
            },
        )

        ws2 = playground.create_workspace(
            name="custom", file_store=custom_store
        )
        self.assertEqual(ws2.default_file_store, custom_store)

    def test_compute_dynamic_data_noop(self) -> None:
        """Test compute_dynamic_data with empty result."""
        playground = Playground()
        wr = playground.create_work_request(
            task_type=TaskTypes.WORKER, task_name="noop"
        )
        playground.compute_dynamic_data(wr)
        self.assertIsNone(wr.dynamic_task_data)

    def test_compute_dynamic_data_sbuild(self) -> None:
        """Test compute_dynamic_data with nonempty result."""
        playground = Playground()
        source = playground.create_source_artifact()
        environment_item = playground.create_debian_environment()
        assert environment_item.artifact is not None
        environment = environment_item.artifact
        wr = playground.create_sbuild_work_request(
            source=source,
            environment=environment,
        )
        playground.compute_dynamic_data(wr)
        self.assertEqual(
            wr.dynamic_task_data,
            {
                "environment_id": environment.pk,
                "input_extra_binary_artifacts_ids": [],
                "input_source_artifact_id": source.pk,
                "binnmu_maintainer": (
                    "Debusine <noreply@debusine-playground.net>"
                ),
            },
        )

    def test_create_source_artifact(self) -> None:
        """Test creating a source artifact."""
        playground = Playground()
        source = playground.create_source_artifact()
        self.assertEqual(source.category, ArtifactCategory.SOURCE_PACKAGE)
        self.assertEqual(source.workspace, playground.get_default_workspace())
        self.assertEqual(source.files.count(), 0)
        self.assertEqual(source.created_by, playground.get_default_user())
        self.assertIsNone(source.created_by_work_request)

        artifact = DebianSourcePackage(**source.data)
        self.assert_source_artifact_equal(artifact, "hello", "1.0-1")

    def test_create_source_artifact_with_files(self) -> None:
        """Test creating a source artifact with its files."""
        playground = Playground()
        source = playground.create_source_artifact(create_files=True)
        files = sorted(
            FileInArtifact.objects.filter(artifact=source),
            key=lambda f: f.path,
        )
        self.assertEqual(len(files), 3)
        self.assertEqual(files[0].path, "hello_1.0-1.debian.tar.xz")
        self.assertEqual(files[1].path, "hello_1.0-1.dsc")
        self.assertEqual(files[2].path, "hello_1.0.orig.tar.gz")

    def test_create_build_log_artifact(self) -> None:
        """Test creating a build log artifact."""
        playground = Playground()
        buildlog = playground.create_build_log_artifact()
        self.assertEqual(buildlog.category, ArtifactCategory.PACKAGE_BUILD_LOG)
        self.assertEqual(buildlog.workspace, playground.get_default_workspace())
        self.assertEqual(buildlog.files.count(), 1)
        self.assertEqual(buildlog.created_by, playground.get_default_user())
        self.assertIsNone(buildlog.created_by_work_request)

        artifact = DebianPackageBuildLog(**buildlog.data)
        self.assertEqual(artifact.source, "hello")
        self.assertEqual(artifact.version, "1.0-1")
        self.assertEqual(artifact.filename, "hello_1.0-1_amd64.buildlog")

        file = buildlog.files.first()
        assert file is not None
        backend = buildlog.workspace.default_file_store.get_backend_object()
        with backend.get_stream(file) as fd:
            self.assertEqual(
                fd.readlines()[3].decode(),
                "Line 4 of hello_1.0-1_amd64.buildlog\n",
            )

    def test_create_build_log_artifact_custom(self) -> None:
        """Test creating a build log artifact with custom arguments."""
        test_contents = b"test contents"
        playground = Playground()
        user = User.objects.create_user(
            username="custom", email="custom@example.org"
        )
        work_request = playground.create_work_request(created_by=user)
        buildlog = playground.create_build_log_artifact(
            source="test",
            version="2.0",
            build_arch="arm64",
            work_request=work_request,
            contents=test_contents,
        )
        self.assertEqual(buildlog.category, ArtifactCategory.PACKAGE_BUILD_LOG)
        self.assertEqual(buildlog.workspace, playground.get_default_workspace())
        self.assertEqual(buildlog.files.count(), 1)
        self.assertEqual(buildlog.created_by, user)
        self.assertEqual(buildlog.created_by_work_request, work_request)

        artifact = DebianPackageBuildLog(**buildlog.data)
        self.assertEqual(artifact.source, "test")
        self.assertEqual(artifact.version, "2.0")
        self.assertEqual(artifact.filename, "test_2.0_arm64.buildlog")

        file = buildlog.files.first()
        assert file is not None
        backend = buildlog.workspace.default_file_store.get_backend_object()
        with backend.get_stream(file) as fd:
            self.assertEqual(fd.read(), test_contents)

    def test_create_build_log_artifact_custom_user(self) -> None:
        """Test creating a build log artifact with custom user."""
        playground = Playground()
        user = User.objects.create_user(
            username="custom", email="custom@example.org"
        )
        work_request = playground.create_work_request()
        buildlog = playground.create_build_log_artifact(
            work_request=work_request,
            created_by=user,
        )
        self.assertEqual(buildlog.created_by, user)
        self.assertEqual(buildlog.created_by_work_request, work_request)

    def test_create_debian_env_collection_defaults(self) -> None:
        """Test create_debian_environments_collection."""
        playground = Playground()
        env = playground.create_debian_environments_collection()
        self.assertEqual(env.name, "debian")
        self.assertEqual(env.category, CollectionCategory.ENVIRONMENTS)
        self.assertEqual(env.workspace, playground.get_default_workspace())

    def test_create_debian_env_collection_custom(self) -> None:
        """Test create_debian_environments_collection."""
        playground = Playground()
        workspace = playground.create_workspace(name="custom")
        env = playground.create_debian_environments_collection(
            name="ubuntu", workspace=workspace
        )
        self.assertEqual(env.name, "ubuntu")
        self.assertEqual(env.category, CollectionCategory.ENVIRONMENTS)
        self.assertEqual(env.workspace, workspace)

    def test_create_debian_env_defaults(self) -> None:
        """Test create_debian_environment."""
        playground = Playground()
        env_item = playground.create_debian_environment()
        self.assertEqual(env_item.category, ArtifactCategory.SYSTEM_TARBALL)
        self.assertIsNotNone(env_item.artifact)
        self.assertEqual(
            env_item.parent_collection,
            playground.create_debian_environments_collection(),
        )
        self.assertEqual(
            env_item.created_by_user, playground.get_default_user()
        )
        self.assertEqual(
            env_item.data,
            {
                'architecture': 'amd64',
                'backend': 'unshare',
                'codename': 'bookworm',
                'variant': 'apt',
            },
        )

        env = env_item.artifact
        assert env is not None
        self.assertEqual(env.category, ArtifactCategory.SYSTEM_TARBALL)
        self.assertEqual(env.workspace, playground.get_default_workspace())
        self.assertEqual(
            env.data,
            {
                "architecture": "amd64",
                "codename": "bookworm",
                'filename': 'test',
                'mirror': 'https://deb.debian.org',
                'pkglist': [],
                'variant': 'apt',
                'vendor': 'Debian',
                "with_dev": True,
                'with_init': True,
            },
        )

    def test_create_debian_env_custom(self) -> None:
        """Test create_debian_environment with custom args."""
        playground = Playground()
        workspace = playground.create_workspace(name="custom")
        collection = playground.create_debian_environments_collection(
            workspace=workspace
        )
        user = User.objects.create_user(
            username="custom", email="custom@example.org"
        )
        env_item = playground.create_debian_environment(
            workspace=workspace, collection=collection, user=user
        )
        self.assertEqual(env_item.category, ArtifactCategory.SYSTEM_TARBALL)
        self.assertIsNotNone(env_item.artifact)
        self.assertEqual(env_item.parent_collection, collection)
        self.assertEqual(env_item.created_by_user, user)
        self.assertEqual(
            env_item.data,
            {
                'architecture': 'amd64',
                'backend': 'unshare',
                'codename': 'bookworm',
                'variant': 'apt',
            },
        )

        env = env_item.artifact
        assert env is not None
        self.assertEqual(env.category, ArtifactCategory.SYSTEM_TARBALL)
        self.assertEqual(env.workspace, workspace)
        self.assertEqual(
            env.data,
            {
                "architecture": "amd64",
                "codename": "bookworm",
                'filename': 'test',
                'mirror': 'https://deb.debian.org',
                'pkglist': [],
                "variant": "apt",
                'variant': 'apt',
                'vendor': 'Debian',
                "with_dev": True,
                'with_init': True,
            },
        )

    def test_create_debian_env_reuse(self) -> None:
        """Test object reuse of create_debian_environment."""
        playground = Playground()
        env_item1 = playground.create_debian_environment()
        env_item2 = playground.create_debian_environment()
        self.assertEqual(env_item1, env_item2)

        env_item2 = playground.create_debian_environment(
            environment=env_item1.artifact
        )
        self.assertEqual(env_item1, env_item2)

    def test_create_debian_env_image(self) -> None:
        """Test create_debian_environment for images."""
        playground = Playground()
        env_item = playground.create_debian_environment(
            category=ArtifactCategory.SYSTEM_IMAGE
        )
        self.assertEqual(env_item.category, ArtifactCategory.SYSTEM_IMAGE)
        self.assertIsNotNone(env_item.artifact)
        self.assertEqual(
            env_item.parent_collection,
            playground.create_debian_environments_collection(),
        )
        self.assertEqual(
            env_item.data,
            {
                'architecture': 'amd64',
                'backend': 'unshare',
                'codename': 'bookworm',
                'variant': 'apt',
            },
        )

        env = env_item.artifact
        assert env is not None
        self.assertEqual(env.category, ArtifactCategory.SYSTEM_IMAGE)
        self.assertEqual(env.workspace, playground.get_default_workspace())
        self.assertEqual(
            env.data,
            {
                "architecture": "amd64",
                "codename": "bookworm",
                'filename': 'test',
                'mirror': 'https://deb.debian.org',
                'pkglist': [],
                'variant': 'apt',
                'vendor': 'Debian',
                "with_dev": True,
                'with_init': True,
            },
        )

    def test_create_sbuild_work_request(self) -> None:
        """Test creating a sbuild work request."""
        playground = Playground()
        source = playground.create_source_artifact()
        environment_item = playground.create_debian_environment()
        assert environment_item.artifact is not None
        wr = playground.create_sbuild_work_request(
            source=source, environment=environment_item.artifact
        )

        self.assertEqual(wr.workspace, playground.get_default_workspace())
        self.assertEqual(wr.created_by, playground.get_default_user())
        self.assertEqual(wr.status, WorkRequest.Statuses.PENDING)
        self.assertEqual(wr.result, WorkRequest.Results.NONE)
        self.assertIsNone(wr.worker)
        self.assertEqual(wr.task_type, TaskTypes.WORKER)
        self.assertEqual(wr.task_name, "sbuild")
        self.assertEqual(
            wr.task_data,
            {
                'backend': BackendType.UNSHARE,
                'binnmu': None,
                'build_components': [SbuildBuildComponent.ALL],
                'build_profiles': None,
                'distribution': None,
                'environment': environment_item.artifact.pk,
                'host_architecture': 'all',
                'input': {
                    'extra_binary_artifacts': (),
                    'source_artifact': source.pk,
                },
            },
        )
        self.assertIsNone(wr.dynamic_task_data)
        self.assertIsNone(wr.parent)

    def test_simulate_package_build(self) -> None:
        """Test simulating a whole package build."""
        playground = Playground()
        source = playground.create_source_artifact()
        wr = playground.simulate_package_build(source)

        binaries: list[Artifact] = []
        buildlogs: list[Artifact] = []
        for artifact in Artifact.objects.filter(created_by_work_request=wr):
            match artifact.category:
                case ArtifactCategory.BINARY_PACKAGE:
                    binaries.append(artifact)
                case ArtifactCategory.PACKAGE_BUILD_LOG:
                    buildlogs.append(artifact)
                case _ as unreachable:
                    self.fail(
                        "Work request generated unexpected"
                        f" {unreachable} artifact"
                    )
        self.assertEqual(len(binaries), 1)
        self.assertEqual(len(buildlogs), 1)

        self.assertEqual(wr.workspace, playground.get_default_workspace())
        self.assertIsNotNone(wr.started_at)
        self.assertIsNotNone(wr.completed_at)
        self.assertEqual(wr.created_by, playground.get_default_user())
        self.assertEqual(wr.status, WorkRequest.Statuses.COMPLETED)
        self.assertEqual(wr.result, WorkRequest.Results.SUCCESS)
        self.assertIsNotNone(wr.worker)
        self.assertEqual(wr.task_type, TaskTypes.WORKER)
        self.assertEqual(wr.task_name, "sbuild")
        environment = Artifact.objects.get(pk=wr.task_data["environment"])
        source = Artifact.objects.get(
            pk=wr.task_data["input"]["source_artifact"]
        )
        self.assertEqual(
            wr.task_data,
            {
                'backend': BackendType.UNSHARE,
                'binnmu': None,
                'build_components': [SbuildBuildComponent.ALL],
                'build_profiles': None,
                'distribution': None,
                'environment': environment.pk,
                'host_architecture': 'all',
                'input': {
                    'extra_binary_artifacts': (),
                    'source_artifact': source.pk,
                },
            },
        )
        self.assertEqual(
            SbuildDynamicData.parse_obj(wr.dynamic_task_data),
            SbuildDynamicData(
                environment_id=environment.pk,
                input_source_artifact_id=source.pk,
                binnmu_maintainer="Debusine <noreply@debusine-playground.net>",
            ),
        )
        self.assertIsNone(wr.parent)

        # Test environment
        self.assertEqual(environment.category, ArtifactCategory.SYSTEM_TARBALL)

        # Test source
        self.assertEqual(source.category, ArtifactCategory.SOURCE_PACKAGE)
        self.assert_source_artifact_equal(
            DebianSourcePackage(**source.data), name="hello", version="1.0-1"
        )

        # Test buildlog
        buildlog = buildlogs[0]
        self.assertEqual(buildlog.category, ArtifactCategory.PACKAGE_BUILD_LOG)
        self.assertEqual(
            buildlog.data,
            {
                'filename': 'hello_1.0-1_amd64.buildlog',
                'source': 'hello',
                'version': '1.0-1',
                'bd_uninstallable': None,
            },
        )

        # Test binary
        binary = binaries[0]
        self.assertEqual(binary.category, ArtifactCategory.BINARY_PACKAGE)
        self.assertEqual(
            binary.data,
            {
                'deb_control_files': ['control'],
                'deb_fields': {
                    'Architecture': 'all',
                    'Description': 'Example description',
                    'Maintainer': 'Example Maintainer <example@example.org>',
                    'Package': 'hello',
                    'Version': '1.0-1',
                },
                'srcpkg_name': 'hello',
                'srcpkg_version': '1.0-1',
            },
        )

        # Test artifact relations
        self.assert_artifact_relations(
            buildlog,
            [
                (source, ArtifactRelation.Relations.RELATES_TO),
                (binary, ArtifactRelation.Relations.RELATES_TO),
            ],
        )
        self.assert_artifact_relations(source, [])
        self.assert_artifact_relations(
            binary, [(source, ArtifactRelation.Relations.BUILT_USING)]
        )

    def test_simulate_package_build_custom(self) -> None:
        """Test simulating a whole package build with custom args."""
        playground = Playground()

        workspace = playground.create_workspace(name="custom")
        source = playground.create_source_artifact(
            name="test", version="2.0", workspace=workspace
        )
        env_item = playground.create_debian_environment(
            workspace=workspace,
        )
        environment = env_item.artifact
        assert environment is not None
        worker = playground.create_worker()

        wr = playground.simulate_package_build(
            source,
            environment=environment,
            worker=worker,
        )

        binaries: list[Artifact] = []
        buildlogs: list[Artifact] = []
        for artifact in Artifact.objects.filter(created_by_work_request=wr):
            match artifact.category:
                case ArtifactCategory.BINARY_PACKAGE:
                    binaries.append(artifact)
                case ArtifactCategory.PACKAGE_BUILD_LOG:
                    buildlogs.append(artifact)
                case _ as unreachable:
                    self.fail(
                        "Work request generated unexpected"
                        f" {unreachable} artifact"
                    )
        self.assertEqual(len(binaries), 1)
        self.assertEqual(len(buildlogs), 1)

        self.assertEqual(wr.workspace, workspace)
        self.assertIsNotNone(wr.started_at)
        self.assertIsNotNone(wr.completed_at)
        self.assertEqual(wr.created_by, playground.get_default_user())
        self.assertEqual(wr.status, WorkRequest.Statuses.COMPLETED)
        self.assertEqual(wr.result, WorkRequest.Results.SUCCESS)
        self.assertEqual(wr.worker, worker)
        self.assertEqual(wr.task_type, TaskTypes.WORKER)
        self.assertEqual(wr.task_name, "sbuild")
        self.assertEqual(
            Artifact.objects.get(pk=wr.task_data["environment"]), environment
        )
        self.assertEqual(
            Artifact.objects.get(pk=wr.task_data["input"]["source_artifact"]),
            source,
        )
        self.assertEqual(
            wr.task_data,
            {
                'backend': BackendType.UNSHARE,
                'binnmu': None,
                'build_components': [SbuildBuildComponent.ALL],
                'build_profiles': None,
                'distribution': None,
                'environment': environment.pk,
                'host_architecture': 'all',
                'input': {
                    'extra_binary_artifacts': (),
                    'source_artifact': source.pk,
                },
            },
        )
        self.assertEqual(
            SbuildDynamicData.parse_obj(wr.dynamic_task_data),
            SbuildDynamicData(
                environment_id=environment.pk,
                input_source_artifact_id=source.pk,
                binnmu_maintainer="Debusine <noreply@debusine-playground.net>",
            ),
        )
        self.assertIsNone(wr.parent)

        # Test environment
        self.assertEqual(environment.category, ArtifactCategory.SYSTEM_TARBALL)

        # Test buildlog
        buildlog = buildlogs[0]
        self.assertEqual(buildlog.category, ArtifactCategory.PACKAGE_BUILD_LOG)
        self.assertEqual(
            buildlog.data,
            {
                'filename': 'test_2.0_amd64.buildlog',
                'source': 'test',
                'version': '2.0',
                'bd_uninstallable': None,
            },
        )

        # Test binary
        binary = binaries[0]
        self.assertEqual(binary.category, ArtifactCategory.BINARY_PACKAGE)
        self.assertEqual(
            binary.data,
            {
                'deb_control_files': ['control'],
                'deb_fields': {
                    'Architecture': 'all',
                    'Description': 'Example description',
                    'Maintainer': 'Example Maintainer <example@example.org>',
                    'Package': 'test',
                    'Version': '2.0',
                },
                'srcpkg_name': 'test',
                'srcpkg_version': '2.0',
            },
        )

        # Test artifact relations
        self.assert_artifact_relations(
            buildlog,
            [
                (source, ArtifactRelation.Relations.RELATES_TO),
                (binary, ArtifactRelation.Relations.RELATES_TO),
            ],
        )
        self.assert_artifact_relations(source, [])
        self.assert_artifact_relations(
            binary, [(source, ArtifactRelation.Relations.BUILT_USING)]
        )

    def test_simulate_package_build_workflow(self) -> None:
        """Test simulating a package build in a workflow."""
        playground = Playground()
        template = playground.create_workflow_template("test", "noop")
        workflow = WorkRequest.objects.create_workflow(
            template=template, data={}, created_by=playground.get_default_user()
        )
        source = playground.create_source_artifact()
        wr = playground.simulate_package_build(source, workflow=workflow)
        self.assertEqual(wr.parent, workflow)
        self.assertEqual(
            wr.workflow_data,
            WorkRequestWorkflowData(
                display_name="Build all",
                step="build-all",
            ),
        )

    def test_create_workflow(self) -> None:
        """Test create_workflow."""
        playground = Playground()
        workflow = playground.create_workflow()

        self.assertTrue(workflow.is_workflow)
