# Copyright 2022-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 artifact views."""

import io
import re
import tarfile
from datetime import datetime
from pathlib import Path
from types import GeneratorType
from typing import Any, ClassVar
from unittest import mock

from django.contrib.auth import get_user_model
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import Max
from django.http.response import HttpResponseBase
from django.urls import reverse
from django.utils.formats import date_format as django_date_format
from django.utils.http import http_date

from rest_framework import status

from debusine.artifacts.models import ArtifactCategory
from debusine.db.models import (
    Artifact,
    FileStore,
    Token,
    User,
    default_workspace,
)
from debusine.server.file_backend.interface import FileBackendInterface
from debusine.server.file_backend.local import LocalFileBackend
from debusine.test.django import TestCase
from debusine.web.views.artifacts import ArtifactDetailView, DownloadPathView
from debusine.web.views.tests.utils import ViewTestMixin


class ArtifactDetailViewTests(TestCase):
    """Tests for the ArtifactDetailView class."""

    token: ClassVar[Token]
    path_in_artifact: ClassVar[str]
    file_size: ClassVar[int]
    artifact: ClassVar[Artifact]
    files_contents: ClassVar[dict[str, bytes]]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up the test fixture."""
        super().setUpTestData()
        cls.token = cls.playground.create_token_enabled()
        cls.path_in_artifact = "README.md"
        cls.file_size = 100
        cls.artifact, cls.files_contents = cls.playground.create_artifact(
            [cls.path_in_artifact, "AUTHORS"],
            files_size=cls.file_size,
            expiration_delay=1,
            create_files=True,
        )

    def _get(
        self,
        pk: int | None = None,
        include_token: bool = True,
    ) -> HttpResponseBase:
        """GET request on the ArtifactDetail view."""
        headers: dict[str, Any] = {}
        if pk is None:
            pk = self.artifact.pk
        if include_token:
            headers["HTTP_TOKEN"] = self.token.key
        return self.client.get(
            reverse("artifacts:detail", kwargs={"artifact_id": pk}), **headers
        )

    def test_invalid_artifact_id(self):
        """Test viewing an artifact ID that does not exist."""
        artifact_id = Artifact.objects.aggregate(Max("id"))['id__max'] + 1
        response = self._get(pk=artifact_id)
        self.assertContains(
            response,
            f"Artifact {artifact_id} does not exist",
            status_code=status.HTTP_404_NOT_FOUND,
        )

    def test_check_permissions_denied(self):
        """Permission denied: no token, logged user or public workspace."""
        response = self._get(include_token=False)
        self.assertContains(
            response,
            DownloadPathView.permission_denied_message,
            status_code=status.HTTP_403_FORBIDDEN,
        )

    def test_check_permissions_valid_token_allowed(self):
        """Permission granted: valid token."""
        self.assertFalse(self.artifact.workspace.public)
        response = self._get()
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_check_permissions_public_workspace(self):
        """Permission granted: without a token but it is a public workspace."""
        workspace = self.artifact.workspace
        workspace.public = True
        workspace.save()
        response = self._get()
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_check_permission_logged_user(self):
        """Permission granted: user is logged in."""
        self.assertFalse(self.artifact.workspace.public)
        user = get_user_model().objects.create_user(
            username="testuser", password="testpassword"
        )
        self.client.force_login(user)

        response = self._get()
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_get_success_html_list_root(self):
        """View returns HTML with list of files."""
        work_request = self.playground.create_work_request()
        self.artifact.created_by_work_request = work_request
        self.artifact.save()
        response = self._get()
        self.assertEqual(response.status_code, status.HTTP_200_OK)

        for fileinartifact in self.artifact.fileinartifact_set.all():
            self.assertContains(response, fileinartifact.path)
            self.assertContains(response, fileinartifact.file.size)
            self.assertContains(
                response,
                reverse(
                    "artifacts:download-path",
                    kwargs={
                        "artifact_id": self.artifact.id,
                        "path": fileinartifact.path,
                    },
                ),
            )

        self.assertContains(response, f"Files in artifact {self.artifact.id}")
        self.assertContains(response, "Subdirectory: /")

        self.assertContains(
            response, f"<li>Id: {self.artifact.id}</li>", html=True
        )

        self.assertContains(
            response, f"<li>Category: {self.artifact.category}</li>", html=True
        )
        self.assertContains(
            response,
            f"<li>Workspace: {self.artifact.workspace.name}</li>",
            html=True,
        )
        self.assertContains(
            response,
            f"<li>Created: {_date_format(self.artifact.created_at)}</li>",
            html=True,
        )

        self.assertContains(
            response,
            f"<li>Expire: {_date_format(self.artifact.expire_at)}</li>",
            html=True,
        )

        url_work_request = reverse(
            "work_requests:detail",
            kwargs={"pk": self.artifact.created_by_work_request_id},
        )

        self.assertContains(
            response,
            f"<li>Created by work request: "
            f'<a href="{url_work_request}">'
            f'{self.artifact.created_by_work_request_id}'
            f'</a></li>',
            html=True,
        )

        self.assertContains(
            response,
            f"<li>Data: <pre><code>{self.artifact.data}</code></pre></li>",
            html=True,
        )

        url_download_artifact = (
            reverse(
                "artifacts:download",
                kwargs={"artifact_id": self.artifact.id},
            )
            + "?archive=tar.gz"
        )
        self.assertContains(response, url_download_artifact)

        self.assertIsInstance(
            response.context["elided_page_range"], GeneratorType
        )

    def test_get_success_default_values(self):
        """
        Artifact have some default values.

        View display "Expire: -", "Created by by work request: -" and
        "Created by: -".
        """
        artifact, _ = self.create_artifact([])
        response = self._get(artifact.id)
        self.assertEqual(response.status_code, status.HTTP_200_OK)

        self.assertContains(
            response,
            "<li>Expire: -</li>",
            html=True,
        )

        self.assertContains(
            response,
            "<li>Created by work request: -</li>",
            html=True,
        )

        self.assertContains(
            response,
            "<li>Created by user: -</li>",
            html=True,
        )

    def test_get_success_created_by_user(self):
        """View render "created by user: username"."""
        user = self.playground.get_default_user()
        self.artifact.created_by = user
        self.artifact.save()
        response = self._get()
        self.assertContains(
            response,
            f"<li>Created by user: {user.username}</li>",
            html=True,
        )

    def test_get_success_html_empty_artifact(self):
        """Test HTML output if there are no files in the artifact."""
        artifact, _ = self.create_artifact([])
        response = self._get(artifact.id)
        self.assertContains(response, "The artifact does not have any files.")

    def test_pagination(self):
        """Pagination is set up and rendered by the template."""
        response = self._get()
        self.assertGreaterEqual(ArtifactDetailView.paginate_by, 10)
        self.assertContains(response, '<nav aria-label="pagination">')


class DownloadPathViewTests(TestCase):
    """Tests for the DownloadPathView class."""

    playground_memory_file_store = False

    token: ClassVar[Token]
    path_in_artifact: ClassVar[str]
    file_size: ClassVar[int]
    artifact: ClassVar[Artifact]
    files_contents: ClassVar[dict[str, bytes]]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up the test fixture."""
        super().setUpTestData()
        cls.token = cls.playground.create_token_enabled()
        cls.path_in_artifact = "README.md"
        cls.file_size = 100
        cls.artifact, cls.files_contents = cls.playground.create_artifact(
            [cls.path_in_artifact],
            files_size=cls.file_size,
            create_files=True,
        )

    def get_file(
        self,
        *,
        range_start=None,
        range_end=None,
        range_header=None,
        artifact_id=None,
        path_file=None,
        include_token: bool = True,
    ) -> HttpResponseBase:
        """
        Download file specified in the parameters.

        Unless specified: try to download the whole file (by default
        self.path_file and self.artifact.id).
        """
        headers: dict[str, Any] = {}

        if range_start is not None:
            headers["HTTP_RANGE"] = f"bytes={range_start}-{range_end}"

        if range_header is not None:
            headers["HTTP_RANGE"] = range_header

        if artifact_id is None:
            artifact_id = self.artifact.id

        if path_file is None:
            path_file = self.path_in_artifact

        if include_token:
            headers["HTTP_TOKEN"] = self.token.key

        return self.client.get(
            reverse(
                "artifacts:download-path",
                kwargs={
                    "artifact_id": artifact_id,
                    "path": path_file,
                },
            ),
            **headers,
        )

    def assertFileResponse(self, response, status_code, range_start, range_end):
        """Assert that response has the expected headers and content."""
        self.assertEqual(response.status_code, status_code)
        headers = response.headers

        self.assertEqual(headers["Accept-Ranges"], "bytes")

        file_contents = self.files_contents[self.path_in_artifact]
        response_contents = file_contents[range_start : range_end + 1]

        self.assertEqual(headers["Content-Length"], str(len(response_contents)))

        if len(response_contents) > 0:
            self.assertEqual(
                headers["Content-Range"],
                f"bytes {range_start}-{range_end}/{self.file_size}",
            )

        filename = Path(self.path_in_artifact).name
        self.assertEqual(
            headers["Content-Disposition"], f'inline; filename="{filename}"'
        )

        self.assertEqual(
            b"".join(response.streaming_content), response_contents
        )

    def test_normalise_path(self) -> None:
        """Test ArtifactDetailView.normalise_path."""
        f = DownloadPathView.normalise_path
        self.assertEqual(f(""), "/")
        self.assertEqual(f("/"), "/")
        self.assertEqual(f("."), "/")
        self.assertEqual(f(".."), "/")
        self.assertEqual(f("../"), "/")
        self.assertEqual(f("../../.././../../"), "/")
        self.assertEqual(f("src/"), "/src/")
        self.assertEqual(f("src/.."), "/")
        self.assertEqual(f("/a/b/../c/./d//e/f/../g/"), "/a/c/d/e/g/")

    def test_path_url_does_not_end_in_slash(self):
        """
        URL to download a file does not end in /.

        If ending in / wget or curl -O save the file as index.html
        instead of using Content-Disposition filename.
        """
        url = reverse(
            "artifacts:download-path",
            kwargs={"artifact_id": 10, "path": "package.deb"},
        )
        self.assertFalse(url.endswith("/"))

    def test_check_permissions_denied(self):
        """Permission denied: no token, logged user or public workspace."""
        response = self.get_file(include_token=False)
        self.assertContains(
            response,
            DownloadPathView.permission_denied_message,
            status_code=status.HTTP_403_FORBIDDEN,
        )

    def test_check_permissions_valid_token_allowed(self):
        """Permission granted: valid token."""
        self.assertFalse(self.artifact.workspace.public)
        response = self.get_file(artifact_id=self.artifact.id)
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_check_permissions_public_workspace(self):
        """Permission granted: without a token but it is a public workspace."""
        workspace = self.artifact.workspace
        workspace.public = True
        workspace.save()

        response = self.get_file(include_token=False)

        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_check_permission_logged_user(self):
        """Permission granted: user is logged in."""
        self.assertFalse(self.artifact.workspace.public)
        self.client.force_login(self.playground.get_default_user())

        response = self.client.get(
            reverse(
                "artifacts:download",
                kwargs={
                    "artifact_id": self.artifact.id,
                },
            )
            + "?archive=tar.gz"
        )

        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_get_file(self):
        """Get return the file."""
        response = self.get_file()
        self.assertFileResponse(
            response, status.HTTP_200_OK, 0, self.file_size - 1
        )
        self.assertEqual(
            response.headers["content-type"], "text/markdown; charset=utf-8"
        )

    def test_get_empty_file(self):
        """Test empty downloadable file (which mmap doesn't support)."""
        self.path_in_artifact = "stderr.txt"
        self.file_size = 0
        self.artifact, self.files_contents = self.create_artifact(
            [self.path_in_artifact],
            files_size=self.file_size,
            create_files=True,
        )
        response = self.get_file(path_file=self.path_in_artifact)
        self.assertFileResponse(
            response, status.HTTP_200_OK, 0, self.file_size - 1
        )

    def test_get_file_content_type_for_build(self):
        """Test Content-Type of the view for different file types."""
        file_in_artifact = self.artifact.fileinartifact_set.all().first()

        sub_tests = [
            ("build.changes", "text/plain; charset=utf-8"),
            ("file.log", "text/plain; charset=utf-8"),
            ("file.txt", "text/plain; charset=utf-8"),
            ("hello.build", "text/plain; charset=utf-8"),
            ("hello.buildinfo", "text/plain; charset=utf-8"),
            ("file.sources", "text/plain; charset=utf-8"),
            ("readme.md", "text/markdown; charset=utf-8"),
            ("a.out", "application/octet-stream"),
        ]

        for sub_test in sub_tests:
            with self.subTest(sub_test):
                file_in_artifact.path = sub_test[0]
                file_in_artifact.save()

                response = self.get_file(path_file=sub_test[0])

                self.assertEqual(response.headers["content-type"], sub_test[1])

    def test_get_file_range(self):
        """Get return part of the file (based on Range header)."""
        start = 10
        end = 20
        response = self.get_file(range_start=start, range_end=end)

        self.assertFileResponse(
            response, status.HTTP_206_PARTIAL_CONTENT, start, end
        )

    def test_get_file_content_range_to_end_of_file(self):
        """Server returns a file from a position to the end."""
        start = 5

        end = self.file_size - 1
        response = self.get_file(range_start=start, range_end=end)

        self.assertFileResponse(
            response, status.HTTP_206_PARTIAL_CONTENT, start, end
        )

    def test_get_file_content_range_invalid(self):
        """Get return an error: Range header was invalid."""
        invalid_range_header = "invalid-range"
        response = self.get_file(range_header=invalid_range_header)

        self.assertResponseProblem(
            response, f'Invalid Range header: "{invalid_range_header}"'
        )

    def test_get_path_artifact_does_not_exist(self):
        """Get return 404: artifact not found."""
        non_existing_artifact_id = 0

        response = self.get_file(artifact_id=non_existing_artifact_id)

        self.assertContains(
            response,
            f"Artifact {non_existing_artifact_id} does not exist",
            status_code=status.HTTP_404_NOT_FOUND,
        )

    def test_get_file_file_does_not_exist(self):
        """Get return 404: artifact found but file not found."""
        file_path_no_exist = "does-not-exist"

        response = self.get_file(path_file=file_path_no_exist)

        self.assertContains(
            response,
            f'Artifact {self.artifact.id} does not have '
            f'any file or directory for "{file_path_no_exist}"',
            status_code=status.HTTP_404_NOT_FOUND,
            html=True,
        )

    def test_get_file_range_start_greater_file_size(self):
        """Get return 400: client requested an invalid start position."""
        start = self.file_size + 10
        response = self.get_file(range_start=start, range_end=start + 10)

        self.assertResponseProblem(
            response,
            f"Invalid Content-Range start: {start}. "
            f"File size: {self.file_size}",
        )

    def test_get_file_range_end_is_file_size(self):
        """Get return 400: client requested and invalid end position."""
        response = self.get_file(range_start=0, range_end=self.file_size)
        self.assertResponseProblem(
            response,
            f"Invalid Content-Range end: {self.file_size}. "
            f"File size: {self.file_size}",
        )

    def test_get_file_range_end_greater_file_size(self):
        """Get return 400: client requested an invalid end position."""
        end = self.file_size + 10
        response = self.get_file(range_start=0, range_end=end)

        self.assertResponseProblem(
            response,
            f"Invalid Content-Range end: {end}. "
            f"File size: {self.file_size}",
        )

    def test_get_file_url_redirect(self):
        """
        Get file response: redirect if get_url for the file is available.

        This would happen if the file is stored in a FileStore supporting
        get_url (e.g. an object storage) instead of being served from the
        server's file system.
        """
        destination_url = "https://some-backend.net/file?token=asdf"
        patch = mock.patch.object(
            FileStore, "get_backend_object", autospec=True
        )

        file_backend_mocked = mock.create_autospec(spec=FileBackendInterface)
        file_backend_mocked.get_url.return_value = destination_url

        get_backend_object_mocked = patch.start()
        get_backend_object_mocked.return_value = file_backend_mocked
        self.addCleanup(patch.stop)

        response = self.get_file()
        self.assertEqual(response.status_code, status.HTTP_302_FOUND)
        self.assertEqual(response.url, destination_url)

    def get_artifact(
        self,
        artifact_id: int,
        archive: str | None = None,
        subdirectory: str | None = None,
        **get_kwargs,
    ) -> HttpResponseBase:
        """Request to download an artifact_id."""
        reverse_kwargs: dict[str, Any] = {"artifact_id": artifact_id}
        viewname = "artifacts:download"

        if subdirectory is not None:
            viewname = "artifacts:download-path"
            reverse_kwargs["path"] = subdirectory

        if archive is not None:
            get_kwargs["archive"] = archive

        return self.client.get(
            reverse(viewname, kwargs=reverse_kwargs),
            get_kwargs,
            HTTP_TOKEN=self.token.key,
        )

    def test_get_subdirectory_does_not_exist_404(self):
        """View return HTTP 404 Not Found: no files in the subdirectory."""
        paths = ["README"]
        artifact, _ = self.create_artifact(paths)

        subdirectory = "does-not-exist"
        response = self.get_artifact(artifact.id, "tar.gz", subdirectory)

        self.assertContains(
            response,
            f'Artifact {artifact.id} does not have any file or '
            f'directory for "{subdirectory}"',
            status_code=status.HTTP_404_NOT_FOUND,
            html=True,
        )

    def test_get_subdirectory_only_tar_gz(self):
        """View return tar.gz file with the files from a subdirectory."""
        paths = [
            "README",
            "doc/README",
            "doc/README2",
            "documentation",
            "src/lib/main.c",
            "src/lib/utils.c",
        ]
        artifact, _ = self.create_artifact(paths, create_files=True)

        subdirectory = "src/lib"
        response = self.get_artifact(artifact.id, "tar.gz", subdirectory)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(
            response.headers["Content-Disposition"],
            f'attachment; filename="artifact-{artifact.id}-src_lib.tar.gz"',
        )
        response_content = io.BytesIO(b"".join(response.streaming_content))

        tar = tarfile.open(fileobj=response_content, mode="r:gz")

        expected_files = list(
            filter(lambda x: x.startswith(subdirectory + "/"), paths)
        )
        self.assertEqual(tar.getnames(), expected_files)

    def test_get_unsupported_archive_parameter(self):
        """View return HTTP 400 Bad Request: unsupported archive parameter."""
        archive_format = "tar.xz"
        artifact, _ = self.create_artifact([])

        response = self.get_artifact(artifact.id, archive_format)
        self.assertResponse400(
            response,
            f'Invalid archive parameter: "{archive_format}". '
            'Supported: "tar.gz"',
        )

    def test_path_without_archive(self):
        """Check downloading a path with a missing archive format."""
        response = self.get_artifact(self.artifact.id, archive=None)
        self.assertResponse400(
            response, "archive argument needed when downloading directories"
        )

    def test_get_success_tar_gz(self):
        """View return a .tar.gz file."""
        paths = ["README", "src/main.c"]
        artifact, files_contents = self.create_artifact(
            paths, create_files=True
        )

        response = self.get_artifact(artifact.id, "tar.gz")
        self.assertEqual(response.status_code, status.HTTP_200_OK)

        response_content = io.BytesIO(b"".join(response.streaming_content))

        tar = tarfile.open(fileobj=response_content, mode="r:gz")

        # Check contents of the tar file
        for path in paths:
            self.assertEqual(tar.extractfile(path).read(), files_contents[path])

        # Check relevant headers
        self.assertEqual(
            response.headers["Content-Type"], "application/octet-stream"
        )
        self.assertEqual(
            response.headers["Content-Disposition"],
            f'attachment; filename="artifact-{artifact.id}.tar.gz"',
        )
        self.assertEqual(
            response.headers["Last-Modified"],
            http_date(artifact.created_at.timestamp()),
        )


class CreateArtifactViewTests(TestCase):
    """Tests for CreateArtifactView."""

    playground_memory_file_store = False
    user: ClassVar[User]

    @classmethod
    def setUpTestData(cls):
        """Set up test data."""
        super().setUpTestData()
        cls.user = get_user_model().objects.create_user(
            username="testuser", password="testpassword"
        )

    def verify_create_artifact_with_files(
        self, files: list[SimpleUploadedFile]
    ):
        """
        Test CreateArtifactView via POST to downloads_artifact:create.

        Post the files to create an artifact and verify the created artifact
        and file upload.
        """
        self.client.force_login(self.user)

        # Create a dummy file for testing
        workspace = default_workspace()
        category = ArtifactCategory.WORK_REQUEST_DEBUG_LOGS

        files_to_upload: SimpleUploadedFile | list[SimpleUploadedFile]
        if len(files) == 1:
            files_to_upload = files[0]
        else:
            files_to_upload = files

        post_data = {
            "category": category,
            "workspace": workspace.id,
            "files": files_to_upload,
            "data": "",
        }

        response = self.client.post(reverse("artifacts:create"), post_data)
        self.assertEqual(response.status_code, 302)

        artifact = Artifact.objects.first()
        assert artifact is not None

        self.assertRedirects(
            response,
            reverse(
                "artifacts:detail",
                kwargs={"artifact_id": artifact.id},
            ),
        )

        # Verify artifact
        self.assertEqual(artifact.created_by, self.user)
        self.assertEqual(artifact.workspace, workspace)
        self.assertEqual(artifact.category, category)
        self.assertEqual(artifact.data, {})

        # Verify uploaded files
        self.assertEqual(artifact.fileinartifact_set.count(), len(files))

        local_file_backend = LocalFileBackend(workspace.default_file_store)

        for file_in_artifact, file_to_upload in zip(
            artifact.fileinartifact_set.all().order_by("id"), files
        ):
            with local_file_backend.get_stream(file_in_artifact.file) as file:
                assert file_to_upload.file is not None
                file_to_upload.file.seek(0)
                content = file_to_upload.file.read()
                self.assertEqual(file.read(), content)
                self.assertEqual(file_in_artifact.path, file_to_upload.name)

            self.assertEqual(file_in_artifact.path, file_to_upload.name)

    def test_create_artifact_one_file(self):
        """Post to "user:artifact-create" to create an artifact: one file."""
        file = SimpleUploadedFile("testfile.txt", b"some_file_content")
        self.verify_create_artifact_with_files([file])

    def test_create_artifact_two_files(self):
        """Post to "user:artifact-create" to create an artifact: two files."""
        files = [
            SimpleUploadedFile("testfile.txt", b"some_file_content"),
            SimpleUploadedFile("testfile2.txt", b"another_file_content"),
        ]
        self.verify_create_artifact_with_files(files)

    def test_create_work_request_permission_denied(self):
        """A non-authenticated request cannot get the form (or post)."""
        for method in [self.client.get, self.client.post]:
            with self.subTest(method):
                response = method(reverse("artifacts:create"))
                self.assertContains(
                    response,
                    "You need to be authenticated to create an Artifact",
                    status_code=status.HTTP_403_FORBIDDEN,
                )


class BuildLogViewTests(ViewTestMixin, TestCase):
    """Tests for the BuildLogView class."""

    token: ClassVar[Token]
    filename: ClassVar[str]
    lines: ClassVar[list[str]]
    artifact: ClassVar[Artifact]

    @classmethod
    def setUpTestData(cls) -> None:
        """Set up the test fixture."""
        super().setUpTestData()
        cls.token = cls.playground.create_token_enabled()
        cls.lines = [f"line {number}" for number in range(1, 11)]
        cls.filename = "hello_1.0-1_amd64.buildlog"
        cls.artifact, _ = cls.playground.create_artifact(
            paths={cls.filename: "\n".join(cls.lines).encode()},
            create_files=True,
        )

    def _get(
        self,
        pk: int | None = None,
        include_token: bool = True,
    ) -> HttpResponseBase:
        """GET request on the ArtifactDetail view."""
        headers: dict[str, Any] = {}
        if pk is None:
            pk = self.artifact.pk
        if include_token:
            headers["HTTP_TOKEN"] = self.token.key
        return self.client.get(
            reverse("artifacts:build-log", kwargs={"artifact_id": pk}),
            **headers,
        )

    def test_invalid_artifact_id(self):
        """Test viewing an artifact ID that does not exist."""
        artifact_id = Artifact.objects.aggregate(Max("id"))['id__max'] + 1
        response = self._get(pk=artifact_id)
        self.assertContains(
            response,
            "No artifact found matching the query",
            status_code=status.HTTP_404_NOT_FOUND,
        )

    def test_check_permissions_denied(self):
        """Permission denied: no token, logged user or public workspace."""
        response = self._get(include_token=False)
        self.assertContains(
            response,
            DownloadPathView.permission_denied_message,
            status_code=status.HTTP_403_FORBIDDEN,
        )

    def test_check_permissions_valid_token_allowed(self):
        """Permission granted: valid token."""
        self.assertFalse(self.artifact.workspace.public)
        response = self._get()
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_check_permissions_public_workspace(self):
        """Permission granted: without a token but it is a public workspace."""
        workspace = self.artifact.workspace
        workspace.public = True
        workspace.save()
        response = self._get()
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_check_permission_logged_user(self):
        """Permission granted: user is logged in."""
        self.assertFalse(self.artifact.workspace.public)
        user = get_user_model().objects.create_user(
            username="testuser", password="testpassword"
        )
        self.client.force_login(user)

        response = self._get()
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_get_success_html_list_root(self):
        """View returns HTML with list of files."""
        response = self._get()
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        tree = self.assertHTMLValid(response)

        lines = tree.xpath("//ul[@id='buildlog-lines']")[0]
        self.assertEqual(len(lines.li), 10)

        line = lines.li[4]
        self.assertEqual(line.a.get("id"), "L5")
        self.assertEqual(line.a.get("href"), "#L5")
        self.assertEqual(line.a.text.strip(), "5")
        self.assertEqual(line.span.text.strip(), "line 5")

        title = tree.xpath("//head/title")[0]
        self.assertRegex(title.text.strip(), re.escape(self.filename))

        title = tree.xpath("//h1")[0]
        self.assertEqual(title.text.strip(), self.filename)


def _date_format(dt: datetime) -> str:
    """Return dt datetime formatted with the Django template format."""
    return django_date_format(dt, "DATETIME_FORMAT")
