# Copyright 2021-2022 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 serializers."""

from datetime import datetime, timezone

from django.test import RequestFactory, TestCase, override_settings

from rest_framework.exceptions import ErrorDetail

from debusine.db.models import (
    DEFAULT_WORKSPACE_NAME,
    File,
    WorkRequest,
    Worker,
    default_workspace,
)
from debusine.db.tests.utils import _calculate_hash_from_data
from debusine.server.serializers import (
    ArtifactSerializer,
    ArtifactSerializerResponse,
    FileSerializer,
    WorkRequestCompletedSerializer,
    WorkRequestSerializer,
    WorkerRegisterSerializer,
    WorkflowTemplateSerializer,
)
from debusine.test import TestHelpersMixin, utils


class WorkRequestSerializerTests(TestHelpersMixin, TestCase):
    """Tests for WorkRequestSerializer."""

    def setUp(self):
        """Initialize test."""
        self.token = self.create_token_enabled(with_user=True)

        worker = Worker.objects.create_with_fqdn(
            'worker.lan', self.create_token_enabled()
        )

        self.work_request = self.create_work_request(
            started_at=datetime(2022, 2, 20, 15, 19, 1, 158424, timezone.utc),
            completed_at=datetime(2022, 2, 20, 16, 2, 3, 558425, timezone.utc),
            status=WorkRequest.Statuses.COMPLETED,
            result=WorkRequest.Results.SUCCESS,
            worker=worker,
            task_name='sbuild',
            task_data={'arch': 'x64'},
            created_by=self.token.user,
        )

        self.work_request_serializer = WorkRequestSerializer(self.work_request)

        self.workspace = self.work_request.workspace

    def test_expected_fields(self):
        """Serializer returns the expected fields."""
        self.assertCountEqual(
            self.work_request_serializer.data.keys(),
            {
                'artifacts',
                'completed_at',
                'created_at',
                'created_by',
                'duration',
                'dynamic_task_data',
                'event_reactions',
                'id',
                'priority_adjustment',
                'priority_base',
                'result',
                'started_at',
                'status',
                'task_data',
                'task_name',
                'task_type',
                'worker',
                'workspace',
            },
        )

    def test_serialize_include_artifact_ids_created_by_work_request(self):
        """Assert serialized WorkRequest include artifact ids created by it."""
        artifact_1, _ = self.create_artifact(work_request=self.work_request)
        artifact_2, _ = self.create_artifact(work_request=self.work_request)
        serialized = self.work_request_serializer.data
        self.assertEqual(
            serialized["artifacts"], sorted([artifact_1.id, artifact_2.id])
        )

    def assert_is_valid(
        self, data, only_fields, is_valid_expected, errors=None
    ):
        """Validate data using only_fields and expects is_valid_expected."""
        work_request_serializer = WorkRequestSerializer(
            data=data, only_fields=only_fields
        )
        self.assertEqual(work_request_serializer.is_valid(), is_valid_expected)

        if errors is not None:
            self.assertEqual(work_request_serializer.errors, errors)

    def test_validate_only_fields_invalid(self):
        """Use only_fields with an extra field."""
        data = {
            'task_name': 'sbuild',
            'task_data': {'foo': 'bar'},
            'unwanted_field': 'for testing',
            'unwanted_field2': 'for testing',
            'workspace': self.workspace.name,
            'created_by': self.token.user.id,
        }
        errors = {
            'non_field_errors': [
                ErrorDetail(
                    string='Invalid fields: unwanted_field, unwanted_field2',
                    code='invalid',
                )
            ]
        }
        self.assert_is_valid(
            data,
            ['task_name', 'task_data', 'workspace', 'created_by'],
            False,
            errors,
        )

    def test_validate_only_fields_valid(self):
        """Use only_fields with an extra field in data."""
        data = {
            'task_name': 'sbuild',
            'task_data': {'foo': 'bar'},
            'workspace': self.workspace.name,
            'created_by': self.token.user.id,
        }
        self.assert_is_valid(
            data, ['task_name', 'task_data', 'workspace', 'created_by'], True
        )

    def test_validate_without_only_fields(self):
        """is_valid() return True: not using only_fields."""
        data = {
            'task_name': 'sbuild',
            'workspace': self.workspace.name,
            'created_by': self.token.user.id,
        }
        self.assert_is_valid(data, None, True)

    def test_serialized_workspace_name(self):
        """Serialized work request has workspace name."""
        work_request_serialized = WorkRequestSerializer(self.work_request).data

        self.assertEqual(
            work_request_serialized["workspace"], self.workspace.name
        )


class WorkerRegisterSerializerTests(TestCase):
    """Test for WorkerRegisterSerializer class."""

    def test_expected_fields(self):
        """Expected fields are defined in the serializer."""
        worker_register_serializer = WorkerRegisterSerializer()
        data = worker_register_serializer.data

        self.assertCountEqual(data.keys(), {'token', 'fqdn'})


class WorkRequestCompletedSerializerTests(TestCase):
    """Test for WorkRequestCompletedSerializer class."""

    def test_accept_success(self):
        """Serializer accept {"result": "success" }."""
        work_request_completed_serializer = WorkRequestCompletedSerializer(
            data={'result': 'success'}
        )
        self.assertTrue(work_request_completed_serializer.is_valid())
        self.assertEqual(
            work_request_completed_serializer.validated_data['result'],
            'success',
        )

    def test_not_accept_unknown_result(self):
        """Serializer does not accept unrecognised result."""
        work_request_completed_serializer = WorkRequestCompletedSerializer(
            data={'result': 'something'}
        )
        self.assertFalse(work_request_completed_serializer.is_valid())


def serialized_file():
    """Return a file."""
    return {
        "type": "file",
        "size": 3827,
        "checksums": {
            "sha256": "164a3bc86c0fe9a7aa15cfa9156e9be1124aad69e"
            "a757a011be1f1a13502409f",
            "md5": "7d13cb5b2bee07003d2b69ccbd256e65",
        },
    }


class FileSerializerTests(TestCase):
    """Test for FileSerializer class."""

    def test_serializer_with_valid_data(self):
        """Test File serializer (valid data)."""
        data = serialized_file()

        file_serializer = FileSerializer(data=data)

        self.assertTrue(file_serializer.is_valid())
        self.assertEqual(file_serializer.validated_data, data)

    def test_is_valid_false_missing_file_type(self):
        """Test FileSerializer (invalid data, no file_type)."""
        data = serialized_file()

        del data["type"]

        file_serializer = FileSerializer(data=data)

        self.assertFalse(file_serializer.is_valid())

    def test_is_valid_false_invalid_file_type(self):
        """Test FileSerializer (invalid file type)."""
        file = serialized_file()

        file["type"] = "directory"

        file_serializer = FileSerializer(data=file)

        self.assertFalse(file_serializer.is_valid())

    def test_is_valid_false_invalid_file_size(self):
        """Test FileSerializer (size must be positive)."""
        file = serialized_file()

        file["size"] = -10

        file_serializer = FileSerializer(data=file)

        self.assertFalse(file_serializer.is_valid())


class FileSerializerResponseTests(TestCase):
    """Tests for FileSerializerResponse."""

    def test_url(self):
        """It has the URL of the file."""
        file = serialized_file()
        file["url"] = "https://example.com/some/URL"

        FileSerializer(data=file)


class ArtifactSerializerTests(TestCase):
    """Test for ArtifactSerializer class."""

    def test_is_valid_true(self):
        """Test Artifact serializer (valid data)."""
        category = "artifact-test"

        serialized_artifact = {
            "category": category,
            "workspace": "test",
            "work_request": 5,
            "files": {"AUTHORS": serialized_file()},
            "data": {"key1": "value1", "key2": "value2"},
        }

        artifact_serializer = ArtifactSerializer(data=serialized_artifact)
        self.assertTrue(artifact_serializer.is_valid())
        self.assertEqual(
            artifact_serializer.validated_data, serialized_artifact
        )

    def test_is_valid_true_no_work_request(self):
        """Test Artifact serializer (valid data) with no work_request."""
        category = "artifact-test"

        serialized_artifact = {
            "category": category,
            "workspace": "test",
            "files": {"AUTHORS": serialized_file()},
            "data": {"key1": "value1", "key2": "value2"},
        }
        artifact_serializer = ArtifactSerializer(data=serialized_artifact)
        self.assertTrue(artifact_serializer.is_valid())
        self.assertNotIn("work_space", artifact_serializer.validated_data)

    def test_is_valid_true_work_request_is_none(self):
        """Test Artifact serializer (valid data) with work_request=None."""
        category = "artifact-test"

        serialized_artifact = {
            "category": category,
            "workspace": "test",
            "files": {"AUTHORS": serialized_file()},
            "data": {"key1": "value1", "key2": "value2"},
            "work_request": None,
        }
        artifact_serializer = ArtifactSerializer(data=serialized_artifact)
        self.assertTrue(artifact_serializer.is_valid())
        self.assertNotIn("work_space", artifact_serializer.validated_data)


class ArtifactSerializerResponseTests(TestHelpersMixin, TestCase):
    """Tests for ArtifactSerializerResponse class."""

    def setUp(self):
        """Set up test."""
        self.files_to_add = ["Makefile", "README.txt"]
        self.artifact, self.files = self.create_artifact(
            self.files_to_add, create_files=True, skip_add_files_in_store=True
        )

    @override_settings(ALLOWED_HOSTS=["*"])
    def test_from_artifact(self):
        """from_artifact() return the expected ArtifactSerializerResponse."""
        host = "example.com"
        request = RequestFactory().get("/test", HTTP_HOST=host)
        serializer = ArtifactSerializerResponse.from_artifact(
            self.artifact, request
        )

        download_url = (
            f"http://{host}/artifact/{self.artifact.id}/?archive=tar.gz"
        )
        created_at = utils.date_time_to_isoformat_rest_framework(
            self.artifact.created_at
        )

        files = {}

        for file_name, file_content in self.files.items():
            checksums = {
                File.current_hash_algorithm: _calculate_hash_from_data(
                    file_content
                ).hex()
            }
            files[file_name] = {
                "size": len(file_content),
                "checksums": checksums,
                "type": "file",
                "url": f"http://{host}/artifact/{self.artifact.id}/{file_name}",
            }

        self.assertEqual(
            serializer.data,
            {
                "id": self.artifact.id,
                "workspace": self.artifact.workspace.name,
                "category": self.artifact.category,
                "data": self.artifact.data,
                "created_at": created_at,
                "expire_at": self.artifact.expire_at,
                "download_tar_gz_url": download_url,
                "files_to_upload": self.files_to_add,
                "files": files,
            },
        )

    @override_settings(ALLOWED_HOSTS=["*"])
    def test_build_absolute_download_url(self):
        """_download_url() return the expected URL."""
        artifact_id = 11
        host = "example.com"

        request = RequestFactory().get("/test", HTTP_HOST=host)

        actual = ArtifactSerializerResponse._build_absolute_download_url(
            artifact_id, request
        )
        expected = f"{request.scheme}://{host}/artifact/" f"{artifact_id}/"

        self.assertEqual(actual, expected)


class WorkflowTemplateSerializerTests(TestCase):
    """Tests for WorkflowTemplateSerializer."""

    def test_valid_data(self):
        """Test valid data."""
        data = {
            "name": "wt",
            "workspace": DEFAULT_WORKSPACE_NAME,
            "task_name": "noop",
        }

        serializer = WorkflowTemplateSerializer(data=data)

        self.assertTrue(serializer.is_valid())
        self.assertEqual(
            serializer.validated_data,
            {**data, "workspace": default_workspace()},
        )

    def test_invalid_task_name(self):
        """The serializer rejects an invalid task name."""
        data = {
            "name": "wt",
            "workspace": DEFAULT_WORKSPACE_NAME,
            "task_name": "nonexistent",
        }

        serializer = WorkflowTemplateSerializer(data=data)

        self.assertFalse(serializer.is_valid())
