# 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.

"""Unit tests for Autopkgtest work request plugin."""

import textwrap
from typing import ClassVar, override

from django.http import HttpResponseBase
from django.template.loader import get_template
from django.template.response import SimpleTemplateResponse

from debusine.artifacts import AutopkgtestArtifact
from debusine.artifacts.models import (
    ArtifactCategory,
    CollectionCategory,
    DebianAutopkgtest,
    DebianAutopkgtestSource,
)
from debusine.db.models import Artifact, WorkRequest, Workspace
from debusine.db.playground import scenarios
from debusine.tasks.models import (
    AutopkgtestData,
    AutopkgtestDynamicData,
    AutopkgtestInput,
    BackendType,
    LookupMultiple,
    LookupSingle,
)
from debusine.test.django import TestCase
from debusine.web.views.autopkgtest import (
    AutopkgtestLogParser,
    AutopkgtestTab,
    AutopkgtestViewWorkRequestPlugin,
)
from debusine.web.views.files import LogFileWidget
from debusine.web.views.work_request import Tab


class AutopkgtestViewWorkRequestPluginTests(TestCase):
    """Tests for AutopkgtestViewWorkRequestPlugin."""

    environment_artifact: ClassVar[Artifact]
    source_artifact: ClassVar[Artifact]

    @override
    @classmethod
    def setUpTestData(cls) -> None:
        super().setUpTestData()
        cls.environment_artifact, _ = cls.playground.create_artifact(
            paths=["system.tar.xz"],
            category=ArtifactCategory.SYSTEM_TARBALL,
            data={
                "vendor": "debian",
                "codename": "bookworm",
                "architecture": "amd64",
                "variant": "",
            },
            create_files=True,
        )
        cls.source_artifact, _ = cls.playground.create_artifact(
            paths=["hello.dsc"],
            category=ArtifactCategory.SOURCE_PACKAGE,
            data={"name": "hello", "version": "1.0", "type": "dpkg"},
            create_files=True,
        )

    def get_tab(self, work_request: WorkRequest) -> AutopkgtestTab:
        """Return the tab for a work request or a plugin."""
        plugin = AutopkgtestViewWorkRequestPlugin(work_request)
        tabs: list[Tab] = []
        plugin.add_tabs(tabs)
        self.assertEqual(len(tabs), 1)
        tab = tabs[0]
        assert isinstance(tab, AutopkgtestTab)
        self.assertEqual(tab.name, "autopkgtest")
        self.assertEqual(tab.label, "Autopkgtest")
        return tab

    def create_work_request_for_artifacts(
        self,
        environment_artifact: Artifact,
        source_artifact: Artifact,
        binary_artifacts: list[Artifact],
        context_artifacts: list[Artifact] | None = None,
        workspace: Workspace | None = None,
    ) -> WorkRequest:
        """Create an autopkgtest work request for some artifacts."""
        environment_lookup: int | str
        if workspace is None:
            workspace = self.playground.get_default_workspace()
        if "vendor" in environment_artifact.data:
            environments_collection = self.playground.create_collection(
                environment_artifact.data["vendor"],
                CollectionCategory.ENVIRONMENTS,
                workspace=workspace,
            )
            environments_collection.manager.add_artifact(
                environment_artifact,
                user=self.playground.get_default_user(),
                variables={"backend": BackendType.UNSHARE},
            )
            environment_lookup = "{vendor}/match:codename={codename}".format(
                **environment_artifact.data
            )
        else:
            environment_lookup = environment_artifact.id
        task_data_input = AutopkgtestInput(
            source_artifact=source_artifact.id,
            binary_artifacts=LookupMultiple.model_validate(
                [binary_artifact.id for binary_artifact in binary_artifacts]
            ),
        )
        if context_artifacts:
            task_data_input.context_artifacts = LookupMultiple.model_validate(
                [context_artifact.id for context_artifact in context_artifacts]
            )

        return self.playground.create_worker_task(
            result=WorkRequest.Results.SUCCESS,
            task_data=AutopkgtestData(
                build_architecture="amd64",
                environment=environment_lookup,
                input=task_data_input,
            ),
            task_name="autopkgtest",
            workspace=workspace,
        )

    def create_autopkgtest_artifact(
        self, work_request: WorkRequest
    ) -> Artifact:
        """Create an AutopkgtestArtifact for a work request."""
        artifact, _ = self.playground.create_artifact(
            paths=["log", "summary"],
            work_request=work_request,
            create_files=True,
        )
        artifact.category = AutopkgtestArtifact._category
        artifact.data = {
            "results": {
                "command1": {"status": "PASS"},
                "command2": {"status": "FAIL", "details": "partial"},
            },
            "cmdline": "/usr/bin/hello",
            "source_package": {
                "name": "hello",
                "version": "1.0",
                "url": "https://example.org/hello.deb",
            },
            "architecture": "amd64",
            "distribution": "debian:sid",
        }
        artifact.save()
        return artifact

    def test_is_enabled(self) -> None:
        work_request: WorkRequest = self.create_work_request_for_artifacts(
            self.environment_artifact, self.source_artifact, []
        )
        autopkgtest_view = AutopkgtestViewWorkRequestPlugin(work_request)
        self.assertTrue(autopkgtest_view.is_enabled())

    def test_get_context_data_simple(self) -> None:
        """get_context_data returns the expected data for a simple case."""
        binary_artifact_1, _ = self.playground.create_artifact(
            paths=["hello-1.deb"], create_files=True
        )
        binary_artifact_2, _ = self.playground.create_artifact(
            paths=["hello-2.deb"], create_files=True
        )
        work_request: WorkRequest = self.create_work_request_for_artifacts(
            self.environment_artifact,
            self.source_artifact,
            [binary_artifact_1, binary_artifact_2],
        )
        work_request.dynamic_task_data = AutopkgtestDynamicData(
            environment_id=self.environment_artifact.id,
            input_source_artifact_id=self.source_artifact.id,
            input_binary_artifacts_ids=[
                binary_artifact_1.id,
                binary_artifact_2.id,
            ],
        ).model_dump()
        work_request.save()
        result_artifact = self.create_autopkgtest_artifact(work_request)
        context_data = self.get_tab(work_request).get_context_data()
        self.assertEqual(
            context_data,
            {
                "request_data": {
                    "backend": BackendType.AUTO,
                    "binary_artifacts": {
                        "lookup": [binary_artifact_1.id, binary_artifact_2.id],
                        "artifacts": [
                            {
                                "files": ["hello-1.deb"],
                                "id": binary_artifact_1.id,
                                "artifact": binary_artifact_1,
                            },
                            {
                                "files": ["hello-2.deb"],
                                "id": binary_artifact_2.id,
                                "artifact": binary_artifact_2,
                            },
                        ],
                    },
                    "build_architecture": "amd64",
                    "codename": "bookworm",
                    "context_artifacts": {"lookup": [], "artifacts": []},
                    "debug_level": 0,
                    "exclude_tests": [],
                    "extra_repositories": None,
                    "extra_environment": {},
                    "fail_on_scenarios": ["failed"],
                    "include_tests": [],
                    "needs_internet": "run",
                    "source_artifact": {
                        "files": ["hello.dsc"],
                        "id": self.source_artifact.id,
                        "lookup": self.source_artifact.id,
                        "artifact": self.source_artifact,
                    },
                    "source_name": "hello",
                    "source_version": "1.0",
                    "task_description": "hello_1.0_amd64 in debian:bookworm",
                    "timeout": None,
                    "use_packages_from_base_repository": False,
                    "vendor": "debian",
                },
                "result": work_request.result,
                "result_artifact": result_artifact,
                "result_data": [
                    ("command1", "PASS", None),
                    ("command2", "FAIL", "partial"),
                ],
            },
        )

    def test_get_context_data_with_context_artifacts(self) -> None:
        """Any context artifacts show up in the context data."""
        context_artifact_1, _ = self.playground.create_artifact(
            paths=["hello-rdep-1.deb"], create_files=True
        )
        context_artifact_2, _ = self.playground.create_artifact(
            paths=["hello-rdep-2.deb"], create_files=True
        )
        work_request: WorkRequest = self.create_work_request_for_artifacts(
            self.environment_artifact,
            self.source_artifact,
            [],
            context_artifacts=[context_artifact_1, context_artifact_2],
        )
        work_request.dynamic_task_data = AutopkgtestDynamicData(
            environment_id=self.environment_artifact.id,
            input_source_artifact_id=self.source_artifact.id,
            input_binary_artifacts_ids=[],
            input_context_artifacts_ids=[
                context_artifact_1.id,
                context_artifact_2.id,
            ],
        ).model_dump()
        work_request.save()
        context_data = self.get_tab(work_request).get_context_data()

        self.assertEqual(
            context_data["request_data"]["context_artifacts"],
            {
                "lookup": [context_artifact_1.id, context_artifact_2.id],
                "artifacts": [
                    {
                        "files": ["hello-rdep-1.deb"],
                        "id": context_artifact_1.id,
                    },
                    {
                        "files": ["hello-rdep-2.deb"],
                        "id": context_artifact_2.id,
                    },
                ],
            },
        )

    def test_context_data_no_artifacts(self) -> None:
        """If there are no output artifacts, context data omits that info."""
        work_request: WorkRequest = self.create_work_request_for_artifacts(
            self.environment_artifact, self.source_artifact, []
        )
        context_data = self.get_tab(work_request).get_context_data()
        self.assertCountEqual(context_data.keys(), {"request_data", "result"})

    def test_context_data_no_dynamic_task_data(self) -> None:
        """If there is no dynamic data, context data omits artifact info."""
        work_request: WorkRequest = self.create_work_request_for_artifacts(
            self.environment_artifact, self.source_artifact, []
        )
        context_data = self.get_tab(work_request).get_context_data()
        self.assertIsNone(context_data["request_data"]["codename"])
        self.assertIsNone(context_data["request_data"]["vendor"])
        self.assertEqual(
            context_data["request_data"]["task_description"],
            "UNKNOWN_UNKNOWN_amd64",
        )
        self.assertEqual(
            context_data["request_data"]["source_artifact"],
            {
                "lookup": self.source_artifact.id,
                "id": None,
                "files": [],
                "artifact": None,
            },
        )

    def test_context_data_missing_environment_artifact(self) -> None:
        """If the environment artifact is missing, context data omits it."""
        environment_artifact, _ = self.playground.create_artifact(
            category=ArtifactCategory.SYSTEM_TARBALL
        )
        work_request: WorkRequest = self.create_work_request_for_artifacts(
            environment_artifact, self.source_artifact, []
        )
        work_request.dynamic_task_data = AutopkgtestDynamicData(
            environment_id=environment_artifact.id,
            input_source_artifact_id=self.source_artifact.id,
            input_binary_artifacts_ids=[],
        ).model_dump()
        work_request.save()
        environment_artifact.delete()
        context_data = self.get_tab(work_request).get_context_data()
        self.assertIsNone(context_data["request_data"]["codename"])
        self.assertIsNone(context_data["request_data"]["vendor"])
        self.assertEqual(
            context_data["request_data"]["task_description"], "hello_1.0_amd64"
        )

    def test_context_data_environment_artifact_wrong_category(self) -> None:
        """We cope with the environment artifact having an odd category."""
        environment_artifact, _ = self.playground.create_artifact(
            paths=["system.tar.xz"],
            category=ArtifactCategory.TEST,
            create_files=True,
        )
        work_request: WorkRequest = self.create_work_request_for_artifacts(
            environment_artifact, self.source_artifact, []
        )
        work_request.dynamic_task_data = AutopkgtestDynamicData(
            environment_id=environment_artifact.id,
            input_source_artifact_id=self.source_artifact.id,
            input_binary_artifacts_ids=[],
        ).model_dump()
        work_request.save()
        tab = self.get_tab(work_request)
        context_data = tab.get_context_data()
        self.assertIsNone(context_data["request_data"]["codename"])
        self.assertIsNone(context_data["request_data"]["vendor"])
        self.assertEqual(
            context_data["request_data"]["task_description"], "hello_1.0_amd64"
        )

    def test_context_data_missing_source_artifact(self) -> None:
        """If the source artifact is missing, context data omits its files."""
        work_request: WorkRequest = self.create_work_request_for_artifacts(
            self.environment_artifact, self.source_artifact, []
        )
        work_request.dynamic_task_data = AutopkgtestDynamicData(
            environment_id=self.environment_artifact.id,
            input_source_artifact_id=self.source_artifact.id,
            input_binary_artifacts_ids=[],
        ).model_dump()
        work_request.save()
        source_artifact_id = self.source_artifact.id
        self.source_artifact.files.clear()
        self.source_artifact.delete()
        context_data = self.get_tab(work_request).get_context_data()
        self.assertEqual(
            context_data["request_data"]["source_artifact"],
            {
                "lookup": source_artifact_id,
                "id": source_artifact_id,
                "artifact": None,
                "files": [],
            },
        )

    def test_context_data_source_artifact_wrong_category(self) -> None:
        """We cope with the source artifact having an odd category."""
        self.source_artifact.category = ArtifactCategory.TEST
        self.source_artifact.save()
        work_request: WorkRequest = self.create_work_request_for_artifacts(
            self.environment_artifact, self.source_artifact, []
        )
        work_request.dynamic_task_data = AutopkgtestDynamicData(
            environment_id=self.environment_artifact.id,
            input_source_artifact_id=self.source_artifact.id,
            input_binary_artifacts_ids=[],
        ).model_dump()
        work_request.save()
        context_data = self.get_tab(work_request).get_context_data()
        self.assertIsNone(context_data["request_data"]["source_name"])
        self.assertIsNone(context_data["request_data"]["source_version"])
        self.assertEqual(
            context_data["request_data"]["task_description"],
            "UNKNOWN_UNKNOWN_amd64 in debian:bookworm",
        )

    def test_context_data_missing_binary_artifact(self) -> None:
        """If a binary artifact is missing, context data omits its files."""
        binary_artifact_1, _ = self.playground.create_artifact(
            paths=["hello-1.deb"], create_files=True
        )
        binary_artifact_2, _ = self.playground.create_artifact(
            paths=["hello-2.deb"], create_files=True
        )
        work_request: WorkRequest = self.create_work_request_for_artifacts(
            self.environment_artifact, self.source_artifact, []
        )
        work_request.dynamic_task_data = AutopkgtestDynamicData(
            environment_id=self.environment_artifact.id,
            input_source_artifact_id=self.source_artifact.id,
            input_binary_artifacts_ids=[
                binary_artifact_1.id,
                binary_artifact_2.id,
            ],
        ).model_dump()
        work_request.save()
        binary_artifact_1_id = binary_artifact_1.id
        binary_artifact_1.files.clear()
        binary_artifact_1.delete()
        tab = self.get_tab(work_request)
        context_data = tab.get_context_data()
        self.assertEqual(
            context_data["request_data"]["binary_artifacts"],
            {
                "lookup": work_request.task_data["input"]["binary_artifacts"],
                "artifacts": [
                    {
                        "id": binary_artifact_1_id,
                        "files": [],
                        "artifact": None,
                    },
                    {
                        "id": binary_artifact_2.id,
                        "files": ["hello-2.deb"],
                        "artifact": binary_artifact_2,
                    },
                ],
            },
        )

    def test_get_context_data_skips_incomplete_files(self) -> None:
        """get_context_data() skips incomplete files."""
        source_artifact, _ = self.playground.create_artifact(
            paths=["hello.dsc"],
            category=ArtifactCategory.SOURCE_PACKAGE,
            data={"name": "hello", "version": "1.0", "type": "dpkg"},
            create_files=True,
            skip_add_files_in_store=True,
        )
        work_request: WorkRequest = self.create_work_request_for_artifacts(
            self.environment_artifact, source_artifact, []
        )
        work_request.dynamic_task_data = AutopkgtestDynamicData(
            environment_id=self.environment_artifact.id,
            input_source_artifact_id=source_artifact.id,
            input_binary_artifacts_ids=[],
        ).model_dump()
        work_request.save()

        context_data = self.get_tab(work_request).get_context_data()
        self.assertEqual(
            context_data["request_data"]["source_artifact"]["files"], []
        )

    def assert_contains_artifact_information(
        self,
        response: HttpResponseBase,
        description: str,
        artifact_lookup: LookupSingle,
        artifact_id: int,
    ) -> None:
        """Assert contains information of an artifact: link and files."""
        artifact = Artifact.objects.get(id=artifact_id)
        files = artifact.fileinartifact_set.order_by("path").values_list(
            "path", flat=True
        )
        artifact_path = artifact.get_absolute_url()
        artifact_link = f'<a href="{artifact_path}">#{artifact_id}</a>'
        files_li = "".join([f"<li>{file}</li>" for file in files])
        self.assertContains(
            response,
            f"<li>{description} ({artifact_lookup}: {artifact_link})"
            f"<ul>{files_li}</ul>"
            f"</li>",
            html=True,
        )

    def assert_contains_artifacts_information(
        self,
        response: HttpResponseBase,
        description: str,
        artifact_lookup: LookupMultiple,
        artifact_ids: list[int],
    ) -> None:
        """Assert contains information of a set of artifacts."""
        expected_response = (
            f"<li>{description} ({artifact_lookup.export()})<ul>"
        )
        for artifact_id in artifact_ids:
            artifact = Artifact.objects.get(id=artifact_id)
            files = artifact.fileinartifact_set.order_by("path").values_list(
                "path", flat=True
            )
            artifact_path = artifact.get_absolute_url()
            artifact_link = f'<a href="{artifact_path}">#{artifact_id}</a>'
            files_li = "".join([f"<li>{file}</li>" for file in files])
            expected_response += f"<li>{artifact_link}<ul>{files_li}</ul></li>"
        expected_response += "</ul></li>"
        self.assertContains(response, expected_response, html=True)

    def test_template_output(self) -> None:
        """Generic output of the template."""
        workspace = self.playground.create_workspace(name="Public", public=True)
        environment = self.playground.create_debian_environment(
            workspace=workspace
        )
        assert environment.artifact
        source_artifact = self.playground.create_source_artifact(
            workspace=workspace
        )
        binary_artifact_1 = (
            self.playground.create_minimal_binary_package_artifact(
                package="hello-1"
            )
        )
        binary_artifact_2 = (
            self.playground.create_minimal_binary_package_artifact(
                package="hello-2"
            )
        )
        work_request: WorkRequest = self.create_work_request_for_artifacts(
            environment.artifact,
            source_artifact,
            [binary_artifact_1, binary_artifact_2],
            workspace=workspace,
        )
        work_request.dynamic_task_data = AutopkgtestDynamicData(
            environment_id=environment.artifact.id,
            input_source_artifact_id=source_artifact.id,
            input_binary_artifacts_ids=[
                binary_artifact_1.id,
                binary_artifact_2.id,
            ],
        ).model_dump()
        work_request.save()
        self.create_autopkgtest_artifact(work_request)

        response = self.client.get(work_request.get_absolute_url())
        tree = self.assertResponseHTML(response, dump_on_error=True)
        title = self.assertHasElement(tree, "//head/title")
        self.assertTextContentEqual(
            title, f"debusine - Public - #{work_request.id}: autopkgtest"
        )

        result_output = get_template("web/_work_request-result.html").render(
            {"result": work_request.result}
        )
        self.assertContains(response, f"Result: {result_output}", html=True)

        self.assert_contains_artifact_information(
            response, "Source artifact", source_artifact.id, source_artifact.id
        )

        self.assert_contains_artifacts_information(
            response,
            "Binary artifacts",
            LookupMultiple((binary_artifact_1.id, binary_artifact_2.id)),
            [binary_artifact_1.id, binary_artifact_2.id],
        )

    def test_get_fail_on_scenarios(self) -> None:
        """_get_fail_on_scenarios processes the task's fail_on correctly."""
        sub_tests = [
            ({"failed_test": True}, ["failed"]),
            (
                {
                    "failed_test": False,
                    "flaky_test": True,
                    "skipped_test": False,
                },
                ["flaky"],
            ),
            (
                {
                    "failed_test": True,
                    "flaky_test": True,
                    "skipped_test": True,
                },
                ["failed", "flaky", "skipped"],
            ),
        ]

        for fail_on, expected_scenarios in sub_tests:
            with self.subTest(fail_on=fail_on):
                self.assertEqual(
                    AutopkgtestTab._get_fail_on_scenarios(fail_on),
                    expected_scenarios,
                )

    def test_show_configured_task_data(self) -> None:
        """The view shows configured task data if present."""
        work_request: WorkRequest = self.create_work_request_for_artifacts(
            self.environment_artifact, self.source_artifact, []
        )

        # Try unconfigured
        context_data = self.get_tab(work_request).get_context_data()
        self.assertEqual(
            context_data["request_data"]["build_architecture"], "amd64"
        )

        # Try configured
        work_request.configured_task_data = work_request.task_data.copy()
        work_request.configured_task_data["build_architecture"] = "arm64"
        work_request.save()
        context_data = self.get_tab(work_request).get_context_data()
        self.assertEqual(
            context_data["request_data"]["build_architecture"], "arm64"
        )


class AutopkgtestLogParserTests(TestCase):
    """Tests for AutopkgtestLogParser class."""

    scenario = scenarios.PackageBuildLog()

    def test_parse_log(self) -> None:
        Section = AutopkgtestLogParser.Section
        SubSection = AutopkgtestLogParser.SubSection
        Section.model_rebuild()
        SubSection.model_rebuild()

        sections, number_of_lines = AutopkgtestLogParser(
            self.scenario.autopkgtest_log
        ).parse()
        self.assertEqual(
            number_of_lines, self.scenario.autopkgtest_log.count(b"\n")
        )

        self.assertEqual(
            sections,
            [
                Section(
                    text="Preparation",
                    color="primary",
                    subsections=[
                        SubSection(
                            text="start run",
                            color=None,
                            subsections=[],
                            line=1,
                            result=None,
                            expanded=True,
                            include_in_table_of_contents=True,
                            tooltip=None,
                        ),
                        SubSection(
                            text="test bed setup",
                            color=None,
                            subsections=[],
                            line=6,
                            result=None,
                            expanded=False,
                            include_in_table_of_contents=True,
                            tooltip=None,
                        ),
                        SubSection(
                            text="apt-source",
                            color=None,
                            subsections=[],
                            line=10,
                            result=None,
                            expanded=False,
                            include_in_table_of_contents=True,
                            tooltip=None,
                        ),
                    ],
                ),
                Section(
                    text="Tests",
                    color="primary",
                    subsections=[
                        SubSection(
                            text="preparing testbed pybuild-autopkgtest",
                            color=None,
                            subsections=[],
                            line=15,
                            result=None,
                            expanded=False,
                            include_in_table_of_contents=False,
                            tooltip=None,
                        ),
                        SubSection(
                            text="pybuild-autopkgtest",
                            color="success",
                            subsections=[],
                            line=23,
                            result=None,
                            expanded=False,
                            include_in_table_of_contents=True,
                            tooltip="Pass",
                        ),
                        SubSection(
                            text="preparing testbed test-suite",
                            color=None,
                            subsections=[],
                            line=31,
                            result=None,
                            expanded=False,
                            include_in_table_of_contents=False,
                            tooltip=None,
                        ),
                        SubSection(
                            text="test-suite",
                            color="danger",
                            subsections=[],
                            line=43,
                            result=None,
                            expanded=True,
                            include_in_table_of_contents=True,
                            tooltip="Fail",
                        ),
                        SubSection(
                            text="test-suite",
                            color=None,
                            subsections=[],
                            line=49,
                            result=None,
                            expanded=False,
                            include_in_table_of_contents=True,
                            tooltip=None,
                        ),
                        SubSection(
                            text="preparing testbed test-suite-skip",
                            color="secondary",
                            subsections=[],
                            line=50,
                            result=None,
                            expanded=False,
                            include_in_table_of_contents=False,
                            tooltip="Skip",
                        ),
                    ],
                ),
                Section(
                    text="Closing",
                    color="primary",
                    subsections=[
                        SubSection(
                            text="summary",
                            color=None,
                            subsections=[],
                            line=53,
                            result=None,
                            expanded=True,
                            include_in_table_of_contents=True,
                            tooltip=None,
                        )
                    ],
                ),
            ],
        )

    def test_parse_log_without_apt_source(self) -> None:
        Section = AutopkgtestLogParser.Section
        SubSection = AutopkgtestLogParser.SubSection
        Section.model_rebuild()
        SubSection.model_rebuild()

        log_contents = textwrap.dedent(
            """\
        line
        line
        15s autopkgtest [22:36:01]: test m-a-autopkgtest: preparing testbed
        line
        43s autopkgtest [22:36:29]: test m-a-autopkgtest: [-----------------------
        44s I: No Linux header packages are installed.
        line
        line
        """  # noqa: E501
        ).encode()

        sections, number_of_lines = AutopkgtestLogParser(log_contents).parse()
        self.assertEqual(number_of_lines, log_contents.count(b"\n"))
        self.assertEqual(len(sections), 3)
        self.assertEqual(sections[0].subsections, [])

        # 1 subsection: preparing and the test itself
        self.assertEqual(
            sections[1].subsections,
            [
                SubSection(
                    text="preparing testbed m-a-autopkgtest",
                    color=None,
                    subsections=[],
                    line=3,
                    result=None,
                    expanded=False,
                    include_in_table_of_contents=False,
                    tooltip=None,
                ),
                SubSection(
                    text="m-a-autopkgtest",
                    color=None,
                    subsections=[],
                    line=5,
                    result=None,
                    expanded=False,
                    include_in_table_of_contents=True,
                    tooltip=None,
                ),
            ],
        )
        self.assertEqual(sections[2].subsections, [])


class AutopkgtestArtifactPluginTests(TestCase):
    """Tests for AutopkgtestArtifactPlugin class."""

    scenario = scenarios.PackageBuildLog()

    def test_get_context_data(self) -> None:
        artifact = self.scenario.autopkgtest_log_artifact
        log_file = artifact.fileinartifact_set.get(path="log")
        log_file.content_type = "text/plain; charset=utf-8"
        log_file.save()

        response = self.client.get(artifact.get_absolute_url())

        tree = self.assertResponseHTML(response)

        assert isinstance(response, SimpleTemplateResponse)
        assert response.context_data is not None
        self.assertEqual(
            response.context_data["specialized_tab"],
            {
                "label": "Autopkgtest log",
                "slug": "autopkgtest-log",
                "template": "web/_autopkgtest-artifact-detail.html",
            },
        )

        (sections, _) = AutopkgtestLogParser(
            self.scenario.autopkgtest_log
        ).parse()

        self.assertEqual(response.context_data["sections"], sections)
        FileSection = LogFileWidget.Section

        self.assertEqual(
            response.context_data["log_widget"]._sections,
            [
                FileSection(
                    title="start run",
                    start_line=1,
                    end_line=5,
                    expanded=True,
                ),
                FileSection(
                    title="test bed setup",
                    start_line=6,
                    end_line=9,
                    expanded=False,
                ),
                FileSection(
                    title="apt-source",
                    start_line=10,
                    end_line=14,
                    expanded=False,
                ),
                FileSection(
                    title="preparing testbed pybuild-autopkgtest",
                    start_line=15,
                    end_line=22,
                    expanded=False,
                ),
                FileSection(
                    title="pybuild-autopkgtest",
                    start_line=23,
                    end_line=30,
                    expanded=False,
                ),
                FileSection(
                    title="preparing testbed test-suite",
                    start_line=31,
                    end_line=42,
                    expanded=False,
                ),
                FileSection(
                    title="test-suite",
                    start_line=43,
                    end_line=48,
                    expanded=True,
                ),
                FileSection(
                    title="test-suite",
                    start_line=49,
                    end_line=49,
                    expanded=False,
                ),
                FileSection(
                    title="preparing testbed test-suite-skip",
                    start_line=50,
                    end_line=52,
                    expanded=False,
                ),
                FileSection(
                    title="summary",
                    start_line=53,
                    end_line=57,
                    expanded=True,
                ),
            ],
        )

        # Assert line number rendering is correct (has the ID and content)
        line_number_1 = self.assertHasElement(tree, "//a[@id='L1']")
        self.assertTextContentEqual(line_number_1, "1")

    def test_get_context_data_empty_log(self) -> None:
        data = DebianAutopkgtest(
            results={},
            cmdline="autopkgtest",
            source_package=DebianAutopkgtestSource(
                name="hello", version="1.0", url="https://example.com"
            ),
            architecture="amd64",
            distribution="debian:trixie",
        )

        artifact, _ = self.playground.create_artifact(
            paths={"log": b""},
            category=ArtifactCategory.AUTOPKGTEST,
            create_files=True,
            data=data,
        )
        log_file = artifact.fileinartifact_set.get(path="log")
        log_file.content_type = "text/plain; charset=utf-8"
        log_file.save()

        response = self.client.get(artifact.get_absolute_url())

        self.assertResponseHTML(response)

    def test_autopkgtest_without_log(self) -> None:
        data = DebianAutopkgtest(
            results={},
            cmdline="autopkgtest",
            source_package=DebianAutopkgtestSource(
                name="hello", version="1.0", url="https://example.com"
            ),
            architecture="amd64",
            distribution="debian:trixie",
        )

        artifact, _ = self.playground.create_artifact(
            paths={"log": b""},
            category=ArtifactCategory.AUTOPKGTEST,
            create_files=False,
            data=data,
        )

        response = self.client.get(artifact.get_absolute_url())
        tree = self.assertResponseHTML(response)

        error_paragraph = self.assertHasElement(
            tree, "//div[@id='error-artifact']"
        )

        self.assertTextContentEqual(
            error_paragraph,
            "Cannot display autopkgtest: Artifact does not contain log",
        )

    def test_autopkgtest_with_file_in_artifact_error(self) -> None:
        data = DebianAutopkgtest(
            results={},
            cmdline="autopkgtest",
            source_package=DebianAutopkgtestSource(
                name="hello", version="1.0", url="https://example.com"
            ),
            architecture="amd64",
            distribution="debian:trixie",
        )

        artifact, _ = self.playground.create_artifact(
            paths={"log": b""},
            category=ArtifactCategory.AUTOPKGTEST,
            create_files=True,
            skip_add_files_in_store=True,
            data=data,
        )

        response = self.client.get(artifact.get_absolute_url())
        tree = self.assertResponseHTML(response)

        error = self.assertHasElement(
            tree,
            "//div[@id='error-artifact']",
        )

        self.assertTextContentEqual(
            error,
            'Cannot display autopkgtest: '
            'File "log" has not completed uploading.',
        )
