# 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 the SbuildValidatorMixin class."""

from pathlib import Path
from textwrap import dedent
from typing import override

from debusine.artifacts.models import WorkRequestResults
from debusine.tasks.models import BaseTaskData
from debusine.tasks.sbuild_validator_mixin import SbuildValidatorMixin
from debusine.tasks.tests.helpers import SampleBaseExternalTask
from debusine.test import TestCase
from debusine.utils import calculate_hash


class TaskWithSbuildValidatorMixin(
    SbuildValidatorMixin, SampleBaseExternalTask[BaseTaskData]
):
    """Minimum class to test SbuildValidatorMixin."""

    @override
    def run(self, execute_directory: Path) -> WorkRequestResults:
        """Unused abstract method from BaseExternalTask."""
        raise NotImplementedError()


class SbuildValidatorMixinTests(TestCase):
    """Tests for SbuildValidatorMixin class."""

    @override
    def setUp(self) -> None:
        super().setUp()
        self.sbuild_validator_mixin = TaskWithSbuildValidatorMixin({})

    def test_check_directory_for_consistency_errors_no_errors(self) -> None:
        """check_directory_for_consistency_errors() return no errors."""
        sbuild_directory = self.create_temporary_directory()

        (file1 := sbuild_directory / "file1.deb").write_text("First file")
        (file2 := sbuild_directory / "file2.deb").write_text("Second file")

        # build_file does not cause any error
        (
            build_file := sbuild_directory
            / "hello_2.10-2_amd64-2023-05-08T10:04:40Z.build"
        ).write_text("the build file")

        # build_directory does not cause any error
        (sbuild_directory / "hello_2.10-2_amd64.build").symlink_to(build_file)

        changes_file = sbuild_directory / "hello.changes"

        self.write_dsc_file(sbuild_directory / "hello.dsc", [])

        self.write_changes_file(changes_file, [file1, file2])

        self.assertEqual(
            self.sbuild_validator_mixin.check_directory_for_consistency_errors(
                sbuild_directory
            ),
            [],
        )

    def test_check_directory_for_consistency_errors_no_changes_file(
        self,
    ) -> None:
        """check_directory_for_consistency_errors() handles missing .changes."""
        sbuild_directory = self.create_temporary_directory()

        self.sbuild_validator_mixin.check_directory_for_consistency_errors(
            sbuild_directory
        )

    def test_check_directory_for_consistency_errors_multiple_path_components(
        self,
    ) -> None:
        sbuild_directory = self.create_temporary_directory()
        changes_file = sbuild_directory / "hello.changes"
        changes_file.write_text(
            dedent("""\
                Format: 1.8
                Source: hello
                Version: 1.0
                Files:
                 0000 1 devel optional ../file
                Checksums-Sha1:
                 0000 1 ../file
                Checksums-Sha256:
                 0000 1 ../file
            """)
        )
        expected_errors = [
            "'hello.changes' has an entry in Files with more than one path"
            " component: '../file'"
        ]

        self.assertEqual(
            self.sbuild_validator_mixin.check_directory_for_consistency_errors(
                sbuild_directory
            ),
            expected_errors,
        )

    def test_check_directory_for_consistency_errors_missing_file(self) -> None:
        """check_directory_for_consistency_errors() return one file missing."""
        sbuild_directory = self.create_temporary_directory()

        (file1 := sbuild_directory / "file1.deb").write_text("First file")
        (file2 := sbuild_directory / "file2.deb").write_text("Second file")

        changes_file = sbuild_directory / "hello.changes"

        self.write_changes_file(changes_file, [file1, file2])

        file2.unlink()

        expected_errors = [
            f"File in .changes section {section} does not exist: 'file2.deb'"
            for section in ("Checksums-Sha1", "Checksums-Sha256", "Files")
        ]

        self.assertEqual(
            self.sbuild_validator_mixin.check_directory_for_consistency_errors(
                sbuild_directory
            ),
            expected_errors,
        )

    def test_check_directory_for_consistency_errors_files_in_dsc_no_error(
        self,
    ) -> None:
        """
        check_directory_for_consistency_errors(): no error.

        File is in .dsc and in the directory.
        """
        sbuild_directory = self.create_temporary_directory()

        (file_in_dsc := sbuild_directory / "a_file.txt").write_text("Some file")
        dsc_file = sbuild_directory / "hello.dsc"
        self.write_dsc_file(dsc_file, [file_in_dsc])

        changes_file = sbuild_directory / "hello.changes"
        self.write_changes_file(changes_file, [dsc_file])

        self.assertEqual(
            self.sbuild_validator_mixin.check_directory_for_consistency_errors(
                sbuild_directory
            ),
            [],
        )

    def test_check_directory_for_consistency_errors_bad_dsc_file(self) -> None:
        sbuild_directory = self.create_temporary_directory()

        (dsc_file := sbuild_directory / "hello.dsc").write_text("")

        changes_file = sbuild_directory / "hello.changes"
        self.write_changes_file(changes_file, [dsc_file])

        expected_errors = [f"{dsc_file.name!r} does not contain Source field"]

        self.assertEqual(
            self.sbuild_validator_mixin.check_directory_for_consistency_errors(
                sbuild_directory
            ),
            expected_errors,
        )

    def test_check_directory_for_consistency_errors_invalid_hash(self) -> None:
        """
        check_directory_for_consistency_errors() return an error.

        A file has an invalid hash.
        """
        sbuild_directory = self.create_temporary_directory()

        (file := sbuild_directory / "file1.deb").write_text("First file")
        expected_size = file.stat().st_size
        expected_sha256 = calculate_hash(file, "sha256").hex()
        expected_sha1 = calculate_hash(file, "sha1").hex()
        expected_md5 = calculate_hash(file, "md5").hex()

        changes_file = sbuild_directory / "hello.changes"

        self.write_changes_file(changes_file, [file])

        file.write_text("Something else")

        actual_size = file.stat().st_size
        actual_sha256 = calculate_hash(file, "sha256").hex()
        actual_sha1 = calculate_hash(file, "sha1").hex()
        actual_md5 = calculate_hash(file, "md5").hex()

        def format_error(
            hash_algo: str,
            filename: str,
            expected_hash: str,
            actual_hash: str,
            expected_size: int,
            actual_size: int,
        ) -> str:
            return (
                f'{hash_algo} for file {filename} does not match: '
                f'expected: {expected_hash} actual: {actual_hash}. '
                f'Size expected: {expected_size} actual: {actual_size}'
            )

        expected_errors = [
            format_error(
                "sha1",
                file.name,
                expected_sha1,
                actual_sha1,
                expected_size,
                actual_size,
            ),
            format_error(
                "sha256",
                file.name,
                expected_sha256,
                actual_sha256,
                expected_size,
                actual_size,
            ),
            format_error(
                "md5",
                file.name,
                expected_md5,
                actual_md5,
                expected_size,
                actual_size,
            ),
        ]

        self.assertEqual(
            self.sbuild_validator_mixin.check_directory_for_consistency_errors(
                sbuild_directory
            ),
            expected_errors,
        )

    def test_check_directory_for_consistency_errors_extra_file(self) -> None:
        """
        check_directory_for_consistency_errors() return an error.

        There is an extra file in the directory.
        """
        build_directory = self.create_temporary_directory()
        changes_file = build_directory / "file.changes"

        (file1 := build_directory / "file1.deb").write_text("First file")

        self.write_changes_file(changes_file, [file1])

        (file2 := build_directory / "file2.deb").write_text("Second file")

        self.assertEqual(
            self.sbuild_validator_mixin.check_directory_for_consistency_errors(
                build_directory
            ),
            [
                "Files in directory not referenced in "
                f".changes or .dsc: ['{file2}']"
            ],
        )
