# 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.test import TestHelpersMixin
from debusine.web.views.lintian import LintianView


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

    def setUp(self):
        """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_id": source_artifact.id,
                    "binary_artifacts_ids": [
                        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"."""
        lintian_view = LintianView()

        context_data = lintian_view.get_context_data(self.work_request)

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

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

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

    def test_template_output(self):
        """Generic output of the template."""
        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_id"]
        )

        for binary_artifact_id in task_data_input["binary_artifacts_ids"]:
            self.assert_contains_artifact_information(
                response, "Binary artifact", binary_artifact_id
            )

    def assert_contains_artifact_information(
        self, response, description: str, 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_link})"
            f"<ul>{files_li}</ul>"
            f"</li>",
            html=True,
        )

        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 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_id"]
        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_ids"]
        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)
