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

"""Unit tests for Lintian work request plugin."""
import json
import uuid
from pathlib import Path

from django.template.loader import get_template
from django.test import TestCase
from django.urls import reverse

from debusine.artifacts import LintianArtifact, PackageBuildLog
from debusine.db.models import Artifact, FileInArtifact, WorkRequest
from debusine.tasks.models import (
    LintianDynamicData,
    LookupMultiple,
    LookupSingle,
)
from debusine.test import TestHelpersMixin
from debusine.web.views.lintian import LintianView


class LintianViewTests(TestHelpersMixin, TestCase):
    """Tests for LintianView."""

    def setUp(self) -> None:
        """Set up common objects."""
        self.fail_on_severity = "warning"

        source_artifact, _ = self.create_artifact(
            paths=["hello.dsc"], create_files=True
        )
        binary_artifact_1, _ = self.create_artifact(
            paths=["hello-1.deb"], create_files=True
        )
        binary_artifact_2, _ = self.create_artifact(
            paths=["hello-2.deb"], create_files=True
        )

        self.work_request: WorkRequest = self.create_work_request(
            result=WorkRequest.Results.SUCCESS,
            task_data={
                "input": {
                    "source_artifact": source_artifact.id,
                    "binary_artifacts": [
                        binary_artifact_1.id,
                        binary_artifact_2.id,
                    ],
                },
                "fail_on_severity": self.fail_on_severity,
            },
            task_name="lintian",
        )
        self.package_name = "hello"

        self.package_source_file = "hello_2.10-3.dsc"
        self.package_binary_file_1 = "hello_2.10-3.deb"
        self.package_binary_file_2 = "libhello_2.10-3.deb"
        summary_binary_package = {
            "summary": {
                "package_filename": {
                    "hello": self.package_binary_file_1,
                    "libhello": self.package_binary_file_2,
                }
            }
        }
        self.binary_artifact, _ = self.create_artifact(
            paths=["summary.json", "lintian.txt"], create_files=True
        )
        self.binary_artifact.category = LintianArtifact._category
        self.binary_artifact.data = summary_binary_package
        self.binary_artifact.created_by_work_request = self.work_request
        self.binary_artifact.save()

        summary_source_package = {
            "summary": {"package_filename": {"hello": self.package_source_file}}
        }
        self.source_artifact, _ = self.create_artifact(
            paths=["lintian.txt"],
            create_files=True,
        )
        self.source_artifact.category = LintianArtifact._category
        self.source_artifact.data = summary_source_package
        self.source_artifact.created_by_work_request = self.work_request
        self.source_artifact.save()

        build_log_artifact, _ = self.create_artifact(
            paths=["some-file.log"], create_files=True
        )
        build_log_artifact.category = PackageBuildLog._category
        build_log_artifact.save()

    def add_file_in_artifact(
        self, artifact: Artifact, path: str, contents: bytes
    ) -> None:
        """
        Add file in the artifact.

        :param artifact: artifact that the file is added to
        :param path: path in the artifact
        :param contents: contents written into the file that is added in the
          artifact
        """
        file_backend = (
            artifact.workspace.default_file_store.get_backend_object()
        )
        fileobj = self.create_file_in_backend(file_backend, contents)
        FileInArtifact.objects.create(
            artifact=artifact, path=path, file=fileobj
        )

    def create_analysis(self) -> Path:
        """Create an analysis file with tags (without the summary)."""
        contents = json.dumps(
            {
                "summary": {"package_filename": {"hello": "hello_2.10-3.dsc"}},
                "tags": [
                    {
                        "severity": "pedantic",
                        "package": "hello",
                        "tag": "license-problem-gfdl-non-official-text",
                        "explanation": "The given source file is licensed...",
                        "comment": "this is a comment",
                        "note": "invariant part is\\n: with no invariant...",
                        "pointer": "debian/copyright",
                    },
                    {
                        "severity": "pedantic",
                        "package": "hello",
                        "tag": "license-problem-gfdl-non-official-text",
                        "explanation": "The given source file is licensed...",
                        "comment": "",
                        "note": "invariant part is: with no invariant...",
                        "pointer": "doc/hello.info",
                    },
                    {
                        "severity": "warning",
                        "package": "hello",
                        "tag": "some-other-tag",
                        "explanation": "",
                        "comment": "",
                        "note": "some-other-tag note...",
                        "pointer": "src/some-other-tag-file",
                    },
                ],
            }
        ).encode("utf-8")
        return self.create_temporary_file(contents=contents)

    def test_get_context_data(self):
        """Test get_context_data() return the expected context data."""
        lintian_txt_path = (
            reverse(
                "artifacts:detail",
                kwargs={"artifact_id": self.binary_artifact.id},
            )
            + "lintian.txt"
        )
        lintian_view = LintianView()

        context_data = lintian_view.get_context_data(self.work_request)

        # Verify "tags" and "tags_count_severity" in separate methods
        context_data.pop("tags")
        context_data.pop("tags_count_severity")
        context_data.pop("request_data")

        self.assertEqual(
            context_data,
            {
                "result": self.work_request.result,
                "lintian_txt_path": lintian_txt_path,
                "fail_on_severity": self.fail_on_severity,
            },
        )

    def test_get_context_data_verify_tag_count_severity(self):
        """Test get_context_data(): verify "tags_count_severity"."""
        self.binary_artifact.data["summary"] = {
            "package_filename": {"hello": "hello_2.10-3.deb"},
            "tags_count_by_severity": {
                "classification": 0,
                "error": 0,
                "experimental": 0,
                "info": 0,
                "overridden": 0,
                "pedantic": 2,
                "warning": 1,
            },
        }
        self.binary_artifact.save()

        self.source_artifact.data["summary"] = {
            "package_filename": {"hello": "hello_2.10-3.dsc"},
            "tags_count_by_severity": {
                "classification": 0,
                "error": 0,
                "experimental": 0,
                "info": 0,
                "overridden": 5,
                "pedantic": 1,
                "warning": 0,
            },
        }
        self.source_artifact.save()

        lintian_view = LintianView()

        context_data = lintian_view.get_context_data(self.work_request)

        self.assertEqual(
            context_data["tags_count_severity"],
            {
                "classification": 0,
                "error": 0,
                "experimental": 0,
                "info": 0,
                "overridden": 5,
                "pedantic": 3,
                "warning": 1,
            },
        )

    def test_get_context_data_verify_tags(self):
        """Test get_context_data(): verify "tags"."""
        self.add_file_in_artifact(
            self.binary_artifact,
            "analysis.json",
            self.create_analysis().read_bytes(),
        )

        lintian_view = LintianView()

        context_data = lintian_view.get_context_data(self.work_request)

        expected = {
            "hello_2.10-3.dsc": {
                "license-problem-gfdl-non-official-text": {
                    "explanation": "The given source file is licensed...",
                    "severity": "pedantic",
                    "occurrences": [
                        {
                            "pointer": "debian/copyright",
                            "note": "invariant part is\n: with no invariant...",
                            "comment": "this is a comment",
                        },
                        {
                            "pointer": "doc/hello.info",
                            "note": "invariant part is: with no invariant...",
                            "comment": "",
                        },
                    ],
                },
                "some-other-tag": {
                    "explanation": "",
                    "severity": "warning",
                    "occurrences": [
                        {
                            "pointer": "src/some-other-tag-file",
                            "note": "some-other-tag note...",
                            "comment": "",
                        }
                    ],
                },
            }
        }

        actual = {
            filename: {tag_name: tag.dict() for tag_name, tag in tags.items()}
            for filename, tags in context_data["tags"].items()
        }

        uuid1 = actual["hello_2.10-3.dsc"][
            "license-problem-gfdl-non-official-text"
        ].pop("uuid")
        self.assertIsInstance(uuid1, uuid.UUID)

        uuid2 = actual["hello_2.10-3.dsc"]["some-other-tag"].pop("uuid")
        self.assertIsInstance(uuid2, uuid.UUID)

        self.assertEqual(actual, expected)

    def test_get_context_data_verify_request(self):
        """Test get_context_data(): verify "request_data"."""
        task_data_input = self.work_request.task_data["input"]
        self.work_request.dynamic_task_data = LintianDynamicData(
            input_source_artifact_id=task_data_input["source_artifact"],
            input_binary_artifacts_ids=task_data_input["binary_artifacts"],
        ).dict(exclude_unset=True)
        self.work_request.save()

        lintian_view = LintianView()

        context_data = lintian_view.get_context_data(self.work_request)

        expected = {
            "source_artifact": {
                "files": ["hello.dsc"],
                "id": task_data_input["source_artifact"],
                "lookup": task_data_input["source_artifact"],
            },
            "binary_artifacts": {
                "lookup": task_data_input["binary_artifacts"],
                "artifacts": [
                    {
                        "files": ["hello-1.deb"],
                        "id": task_data_input["binary_artifacts"][0],
                    },
                    {
                        "files": ["hello-2.deb"],
                        "id": task_data_input["binary_artifacts"][1],
                    },
                ],
            },
        }

        self.assertEqual(context_data["request_data"], expected)

    def test_get_context_data_no_dynamic_task_data(self):
        """If there is no dynamic data, context data omits artifact info."""
        task_data_input = self.work_request.task_data["input"]

        lintian_view = LintianView()

        context_data = lintian_view.get_context_data(self.work_request)

        self.assertEqual(
            context_data["request_data"]["source_artifact"],
            {
                "lookup": task_data_input["source_artifact"],
                "id": None,
                "files": [],
            },
        )
        self.assertEqual(
            context_data["request_data"]["binary_artifacts"],
            {"lookup": task_data_input["binary_artifacts"], "artifacts": []},
        )

    def test_get_context_data_missing_source_artifact(self):
        """If the source artifact is missing, context data omits its files."""
        task_data_input = self.work_request.task_data["input"]
        self.work_request.dynamic_task_data = LintianDynamicData(
            input_source_artifact_id=task_data_input["source_artifact"],
            input_binary_artifacts_ids=task_data_input["binary_artifacts"],
        ).dict(exclude_unset=True)
        self.work_request.save()
        source_artifact_id = task_data_input["source_artifact"]
        source_artifact = Artifact.objects.get(id=source_artifact_id)
        source_artifact.files.clear()
        source_artifact.delete()
        lintian_view = LintianView()

        context_data = lintian_view.get_context_data(self.work_request)

        self.assertEqual(
            context_data["request_data"]["source_artifact"],
            {"lookup": source_artifact_id, "id": None, "files": []},
        )

    def test_get_context_data_missing_binary_artifact(self):
        """If a binary artifact is missing, context data omits its files."""
        task_data_input = self.work_request.task_data["input"]
        self.work_request.dynamic_task_data = LintianDynamicData(
            input_source_artifact_id=task_data_input["source_artifact"],
            input_binary_artifacts_ids=task_data_input["binary_artifacts"],
        ).dict(exclude_unset=True)
        self.work_request.save()
        binary_artifact_0_id = task_data_input["binary_artifacts"][0]
        binary_artifact_0 = Artifact.objects.get(id=binary_artifact_0_id)
        binary_artifact_0.files.clear()
        binary_artifact_0.delete()
        lintian_view = LintianView()

        context_data = lintian_view.get_context_data(self.work_request)

        self.assertEqual(
            context_data["request_data"]["binary_artifacts"],
            {
                "lookup": task_data_input["binary_artifacts"],
                "artifacts": [
                    {"files": [], "id": task_data_input["binary_artifacts"][0]},
                    {
                        "files": ["hello-2.deb"],
                        "id": task_data_input["binary_artifacts"][1],
                    },
                ],
            },
        )

    def test_template_output(self) -> None:
        """Generic output of the template."""
        task_data_input = self.work_request.task_data["input"]
        self.work_request.dynamic_task_data = LintianDynamicData(
            input_source_artifact_id=task_data_input["source_artifact"],
            input_binary_artifacts_ids=task_data_input["binary_artifacts"],
        ).dict(exclude_unset=True)
        self.work_request.save()
        self.add_file_in_artifact(
            self.binary_artifact,
            "analysis.json",
            self.create_analysis().read_bytes(),
        )

        response = self.client.get(
            reverse("work-requests:detail", kwargs={"pk": self.work_request.id})
        )

        work_request_generic_path = (
            reverse("work_requests:detail", kwargs={"pk": self.work_request.id})
            + "?view=generic"
        )
        self.assertContains(
            response,
            f'<a href="{work_request_generic_path}">Generic view</a>',
            html=True,
        )
        result_output = get_template("web/_work_request-result.html").render(
            {"result": self.work_request.result}
        )
        self.assertContains(response, f"Result: {result_output}", html=True)
        self.assertContains(
            response, f"Fail on: {self.fail_on_severity}", html=True
        )

        lintian_txt_path = (
            reverse(
                "artifacts:detail",
                kwargs={"artifact_id": self.binary_artifact.id},
            )
            + "lintian.txt"
        )
        self.assertContains(
            response,
            f'<a href="{lintian_txt_path}">Lintian output</a>',
            html=True,
        )

        # Test code contains the list (testing part of the list) of
        # the summary of the tags
        self.assertContains(
            response,
            "<li>error: 0</li>",
            html=True,
        )
        self.assertContains(
            response,
            "<li>warning: 0</li>",
            html=True,
        )

        task_data_input = self.work_request.task_data["input"]

        self.assert_contains_artifact_information(
            response,
            "Source artifact",
            task_data_input["source_artifact"],
            task_data_input["source_artifact"],
        )

        self.assert_contains_artifacts_information(
            response,
            "Binary artifacts",
            LookupMultiple.parse_obj(task_data_input["binary_artifacts"]),
            task_data_input["binary_artifacts"],
        )

        tag = response.context["tags"]["hello_2.10-3.dsc"][
            "license-problem-gfdl-non-official-text"
        ].occurrences[0]

        tag_output = get_template("web/_lintian_tag.html").render({"tag": tag})
        self.assertContains(response, f"<li>{tag_output}</li>", html=True)

    def assert_contains_artifact_information(
        self,
        response,
        description: str,
        artifact_lookup: LookupSingle,
        artifact_id: int,
    ):
        """Assert contains information of an artifact: link and files."""
        files = (
            Artifact.objects.get(id=artifact_id)
            .fileinartifact_set.order_by("path")
            .values_list("path", flat=True)
        )
        artifact_path = reverse(
            "artifacts:detail", kwargs={"artifact_id": artifact_id}
        )
        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,
        description: str,
        artifact_lookup: LookupMultiple,
        artifact_ids: list[int],
    ):
        """Assert contains information of a set of artifacts."""
        expected_response = (
            f"<li>{description} ({artifact_lookup.export()})<ul>"
        )
        for artifact_id in artifact_ids:
            files = (
                Artifact.objects.get(id=artifact_id)
                .fileinartifact_set.order_by("path")
                .values_list("path", flat=True)
            )
            artifact_path = reverse(
                "artifacts:detail", kwargs={"artifact_id": artifact_id}
            )
            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_include_note_pointer_comment(self):
        """Template output of _lintian_tag.html include all the information."""
        tag = {
            "note": "invariant part is:\nwith no invariant",
            "pointer": "debian/copyright",
            "comment": "upstream",
        }

        rendered = get_template("web/_lintian_tag.html").render({"tag": tag})

        self.assertIn("invariant part is:<br>with no invariant", rendered)
        self.assertIn(f"<code>[{tag['pointer']}]</code>", rendered)
        self.assertIn("Comment:", rendered)
        self.assertIn(f"<pre>{tag['comment']}</pre>", rendered)

    def test_template_output_do_not_include_non_needed_information(self):
        """Template _lintian_tag.html does not output note, comment, pointer."""
        tag = {"note": "", "pointer": "", "comment": ""}
        rendered = get_template("web/_lintian_tag.html").render({"tag": tag})

        self.assertIn("Lintian did not output a note for this tag", rendered)
        self.assertNotIn("<code>", rendered)
        self.assertNotIn("Comment:", rendered)
        self.assertNotIn("<pre></pre>", rendered)

    def test_template_output_lintian_tag_include_pointer(self):
        """."""

    def test_template_output_no_source_artifact(self):
        """The view renders OK if there was no source artifact in the input."""
        del self.work_request.task_data["input"]["source_artifact"]
        self.work_request.dynamic_task_data = LintianDynamicData(
            input_binary_artifacts_ids=self.work_request.task_data["input"][
                "binary_artifacts"
            ],
        ).dict(exclude_unset=True)
        self.work_request.save()

        response = self.client.get(
            reverse("work-requests:detail", kwargs={"pk": self.work_request.id})
        )

        self.assertNotContains(response, "Source artifact", html=True)

    def test_template_output_no_binary_artifacts(self):
        """The view renders OK if there was no binary artifact in the input."""
        del self.work_request.task_data["input"]["binary_artifacts"]
        self.work_request.dynamic_task_data = LintianDynamicData().dict(
            exclude_unset=True
        )
        self.work_request.save()

        response = self.client.get(
            reverse("work-requests:detail", kwargs={"pk": self.work_request.id})
        )

        self.assertNotContains(response, "Binary artifact", html=True)

    def test_find_lintian_txt_url_path_no_artifacts(self):
        """_find_lintian_txt_url_path() with no artifacts: return None."""
        self.assertIsNone(LintianView._find_lintian_txt_url_path([]))

    def test_find_lintian_txt_url_path_artifact_no_files(self):
        """_find_lintian_txt_url_path(), artifact no files: return None."""
        artifact, _ = self.create_artifact()

        self.assertIsNone(LintianView._find_lintian_txt_url_path([artifact]))

    def test_template_output_no_lintian_txt(self):
        """The view renders OK if no lintian.txt was output."""
        self.source_artifact.fileinartifact_set.all().delete()
        self.binary_artifact.fileinartifact_set.all().delete()

        response = self.client.get(
            reverse("work-requests:detail", kwargs={"pk": self.work_request.id})
        )

        self.assertNotContains(response, "Lintian output", html=True)
