# 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 workflow models."""

from abc import ABCMeta, abstractmethod
from typing import Any, override

from debusine.server.workflows.models import (
    AutopkgtestWorkflowData,
    BlhcWorkflowData,
    ChildWorkspaceData,
    DebianPipelineWorkflowData,
    LintianWorkflowData,
    PackagePublishWorkflowData,
    PiupartsWorkflowData,
    QAFailOn,
    QAWorkflowData,
    RegressionTrackingUpdateQAResults,
    RegressionTrackingWorkflowData,
    ReverseDependenciesAutopkgtestWorkflowData,
    SbuildWorkflowData,
)
from debusine.tasks.models import LookupMultiple, LookupSingle, SbuildInput
from debusine.test.django import TestCase


class SbuildWorkflowDataTests(TestCase):
    """Tests for :py:class:`SbuildWorkflowData`."""

    def test_retry_delays(self) -> None:
        """A valid `retry_delays` field is accepted."""
        delays = ["30m", "1h", "2d", "1w"]
        data = SbuildWorkflowData(
            input=SbuildInput(source_artifact=42),
            target_distribution="debian:bookworm",
            architectures=["all"],
            retry_delays=delays,
        )
        self.assertEqual(
            data.model_dump(exclude_unset=True),
            {
                "input": {"source_artifact": 42},
                "target_distribution": "debian:bookworm",
                "architectures": ["all"],
                "retry_delays": delays,
            },
        )

    def test_retry_delays_bad_format(self) -> None:
        """Items in `retry_delays` must be integers followed by m/h/d/w."""
        with self.assertRaisesRegex(
            ValueError, r"String should match pattern.*input_value='2 weeks'"
        ):
            SbuildWorkflowData(
                input=SbuildInput(source_artifact=42),
                target_distribution="debian:bookworm",
                architectures=["all"],
                retry_delays=["2 weeks"],
            )


class RegressionTrackingWorkflowDataTests[WD: RegressionTrackingWorkflowData](
    TestCase, metaclass=ABCMeta
):
    """Common tests for models that handle regression tracking."""

    qa_suite_always_required = False

    @abstractmethod
    def make_data(self, **kwargs: Any) -> WD:
        """Make a data model of the appropriate type."""

    def test_update_qa_results(self) -> None:
        for value, expected in (
            (False, RegressionTrackingUpdateQAResults.NO),
            (True, RegressionTrackingUpdateQAResults.YES),
            ("no", RegressionTrackingUpdateQAResults.NO),
            ("yes", RegressionTrackingUpdateQAResults.YES),
            ("force", RegressionTrackingUpdateQAResults.FORCE),
        ):
            with self.subTest(value=value):
                self.assertEqual(
                    self.make_data(
                        qa_suite="sid@debian:suite",
                        reference_qa_results="sid@debian:qa-results",
                        update_qa_results=value,
                    ).update_qa_results,
                    expected,
                )

        with (
            self.subTest(value="nonsense"),
            self.assertRaisesRegex(
                ValueError,
                "'nonsense' is not a valid RegressionTrackingUpdateQAResults",
            ),
        ):
            self.make_data(update_qa_results="nonsense")

    def test_qa_results_consistency(self) -> None:
        """Test validation of regression-tracking options."""
        self.make_data()
        self.make_data(
            reference_prefix="reference-qa-result|",
            qa_suite="sid@debian:suite",
            reference_qa_results="sid@debian:qa-results",
            enable_regression_tracking=True,
        )
        self.make_data(
            qa_suite="sid@debian:suite",
            reference_qa_results="sid@debian:qa-results",
            update_qa_results=RegressionTrackingUpdateQAResults.YES,
        )
        self.make_data(
            qa_suite="sid@debian:suite",
            reference_qa_results="sid@debian:qa-results",
            update_qa_results=RegressionTrackingUpdateQAResults.FORCE,
        )

        kwargs: dict[str, Any]
        if not self.qa_suite_always_required:
            for kwargs in (
                {"enable_regression_tracking": True},
                {"update_qa_results": RegressionTrackingUpdateQAResults.YES},
                {"update_qa_results": RegressionTrackingUpdateQAResults.FORCE},
            ):
                with (
                    self.subTest(kwargs=str(kwargs)),
                    self.assertRaisesRegex(
                        ValueError,
                        '"qa_suite" is required if "enable_regression_tracking"'
                        ' or "update_qa_results" is set',
                    ),
                ):
                    self.make_data(**kwargs)
        for kwargs in (
            {
                "qa_suite": "sid@debian:suite",
                "enable_regression_tracking": True,
            },
            {
                "qa_suite": "sid@debian:suite",
                "update_qa_results": RegressionTrackingUpdateQAResults.YES,
            },
            {
                "qa_suite": "sid@debian:suite",
                "update_qa_results": RegressionTrackingUpdateQAResults.FORCE,
            },
        ):
            with (
                self.subTest(kwargs=str(kwargs)),
                self.assertRaisesRegex(
                    ValueError,
                    '"reference_qa_results" is required if'
                    ' "enable_regression_tracking" or "update_qa_results" is'
                    ' set',
                ),
            ):
                self.make_data(**kwargs)
        with self.assertRaisesRegex(
            ValueError,
            '"reference_prefix" is required if "enable_regression_tracking" '
            'is set',
        ):
            self.make_data(
                qa_suite="sid@debian:suite",
                reference_qa_results="sid@debian:qa-results",
                enable_regression_tracking=True,
            )


class PiupartsWorkflowDataTests(
    RegressionTrackingWorkflowDataTests[PiupartsWorkflowData]
):
    """Tests for :py:class:`PiupartsWorkflowData`."""

    @override
    def make_data(self, **kwargs: Any) -> PiupartsWorkflowData:
        """Make a `PiupartsWorkflowData`."""
        return PiupartsWorkflowData(
            source_artifact=1,
            binary_artifacts=LookupMultiple((2,)),
            vendor="debian",
            codename="sid",
            **kwargs,
        )


class AutopkgtestWorkflowDataTests(
    RegressionTrackingWorkflowDataTests[AutopkgtestWorkflowData]
):
    """Tests for :py:class:`AutopkgtestWorkflowData`."""

    @override
    def make_data(self, **kwargs: Any) -> AutopkgtestWorkflowData:
        """Make an `AutopkgtestWorkflowData`."""
        return AutopkgtestWorkflowData(
            source_artifact=1,
            binary_artifacts=LookupMultiple((2,)),
            vendor="debian",
            codename="sid",
            **kwargs,
        )


class ReverseDependenciesAutopkgtestWorkflowDataTests(
    RegressionTrackingWorkflowDataTests[
        ReverseDependenciesAutopkgtestWorkflowData
    ]
):
    """Tests for :py:class:`ReverseDependenciesAutopkgtestWorkflowData`."""

    qa_suite_always_required = True

    @override
    def make_data(
        self, **kwargs: Any
    ) -> ReverseDependenciesAutopkgtestWorkflowData:
        """Make a `ReverseDependenciesAutopkgtestWorkflowData`."""
        data: dict[str, Any] = {
            "source_artifact": 1,
            "binary_artifacts": LookupMultiple((2,)),
            "qa_suite": "sid@debian:suite",
            "vendor": "debian",
            "codename": "sid",
        }
        data.update(kwargs)
        return ReverseDependenciesAutopkgtestWorkflowData(**data)


class LintianWorkflowDataTests(
    RegressionTrackingWorkflowDataTests[LintianWorkflowData]
):
    """Tests for :py:class:`LintianWorkflowData`."""

    @override
    def make_data(self, **kwargs: Any) -> LintianWorkflowData:
        """Make a `LintianWorkflowData`."""
        return LintianWorkflowData(
            source_artifact=1,
            binary_artifacts=LookupMultiple((2,)),
            vendor="debian",
            codename="sid",
            **kwargs,
        )


class BlhcWorkflowDataTests(
    RegressionTrackingWorkflowDataTests[BlhcWorkflowData]
):
    """Tests for :py:class:`BlhcWorkflowData`."""

    @override
    def make_data(
        self,
        source_artifact: LookupSingle | None = 1,
        package_build_logs: LookupMultiple | None = LookupMultiple((2,)),
        **kwargs: Any,
    ) -> BlhcWorkflowData:
        """Make a `BlhcWorkflowData`."""
        return BlhcWorkflowData(
            source_artifact=source_artifact,
            package_build_logs=package_build_logs,
            vendor="debian",
            codename="sid",
            **kwargs,
        )

    def test_source_artifact(self) -> None:
        """Test validation of ``source_artifact``."""
        self.make_data(source_artifact=None)
        self.make_data(
            reference_prefix="reference-qa-result|",
            qa_suite="sid@debian:suite",
            reference_qa_results="sid@debian:qa-results",
            enable_regression_tracking=True,
        )

        with self.assertRaisesRegex(
            ValueError,
            '"source_artifact" is required if "enable_regression_tracking" is '
            'set',
        ):
            self.make_data(
                source_artifact=None,
                reference_prefix="reference-qa-result|",
                qa_suite="sid@debian:suite",
                reference_qa_results="sid@debian:qa-results",
                enable_regression_tracking=True,
            )

    def test_package_build_logs(self) -> None:
        self.make_data(
            binary_artifacts=LookupMultiple((1,)), package_build_logs=None
        )
        self.make_data(package_build_logs=LookupMultiple((2,)))

        error_msg = (
            "Exactly one of binary_artifacts or package_build_logs must be set"
        )
        with self.assertRaisesRegex(ValueError, error_msg):
            self.make_data(package_build_logs=None)
        with self.assertRaisesRegex(ValueError, error_msg):
            self.make_data(
                binary_artifacts=LookupMultiple((1,)),
                package_build_logs=LookupMultiple((2,)),
            )


class QAWorkflowDataTests(RegressionTrackingWorkflowDataTests[QAWorkflowData]):
    """Tests for :py:class:`QAWorkflowData`."""

    @override
    def make_data(self, **kwargs: Any) -> QAWorkflowData:
        """Make a `QAWorkflowData`."""
        return QAWorkflowData.model_validate(
            {
                "source_artifact": 1,
                "binary_artifacts": LookupMultiple((2,)),
                "vendor": "debian",
                "codename": "sid",
                **kwargs,
            },
        )

    def test_reverse_dependencies_autopkgtest_consistency(self) -> None:
        """Test reverse-dependencies-autopkgtest validation."""
        self.make_data()
        self.make_data(
            qa_suite="sid@debian:suite",
            enable_reverse_dependencies_autopkgtest=True,
        )

        with self.assertRaisesRegex(
            ValueError,
            '"qa_suite" is required if '
            '"enable_reverse_dependencies_autopkgtest" is set',
        ):
            self.make_data(enable_reverse_dependencies_autopkgtest=True)

    def test_enable_debdiff_consistency(self) -> None:
        """Test enable_debdiff_consistency validation."""
        self.make_data()
        self.make_data(qa_suite="sid@debian:suite", enable_debdiff=True)

        self.make_data()

        with self.assertRaisesRegex(
            ValueError,
            '"qa_suite" is required if "enable_debdiff" is set',
        ):
            self.make_data(enable_debdiff=True)

    def test_default_fail_on(self) -> None:
        """Test default for ``fail_on``."""
        self.assertEqual(self.make_data().fail_on, QAFailOn.FAILURE)
        self.assertEqual(
            self.make_data(
                reference_prefix="reference-qa-result|",
                qa_suite="sid@debian:suite",
                reference_qa_results="sid@debian:qa-results",
                enable_regression_tracking=True,
            ).fail_on,
            QAFailOn.REGRESSION,
        )


class DebianPipelineWorkflowDataTests(TestCase):
    """Tests for :py:class:`DebianPipelineWorkflowData`."""

    def test_reverse_dependencies_autopkgtest_consistency(self) -> None:
        """Test reverse-dependencies-autopkgtest validation."""
        DebianPipelineWorkflowData(
            source_artifact=1, vendor="debian", codename="sid"
        )
        DebianPipelineWorkflowData(
            source_artifact=1,
            vendor="debian",
            codename="sid",
            qa_suite="sid@debian:suite",
            enable_reverse_dependencies_autopkgtest=True,
        )

        with self.assertRaisesRegex(
            ValueError,
            '"qa_suite" is required if '
            '"enable_reverse_dependencies_autopkgtest" is set',
        ):
            DebianPipelineWorkflowData(
                source_artifact=1,
                vendor="debian",
                codename="sid",
                enable_reverse_dependencies_autopkgtest=True,
            )

    def test_enable_debdiff_consistency(self) -> None:
        """Test enable_debdiff validator."""
        DebianPipelineWorkflowData(
            source_artifact=1, vendor="debian", codename="sid"
        )

        with self.assertRaisesRegex(
            ValueError,
            '"qa_suite" is required if "enable_debdiff" is set',
        ):
            DebianPipelineWorkflowData(
                source_artifact=1,
                enable_debdiff=True,
                vendor="debian",
                codename="sid",
            )

    def test_regression_tracking_consistency(self) -> None:
        """Test regression-tracking validation."""
        DebianPipelineWorkflowData(
            source_artifact=1, vendor="debian", codename="sid"
        )
        DebianPipelineWorkflowData(
            source_artifact=1,
            vendor="debian",
            codename="sid",
            qa_suite="sid@debian:suite",
            regression_tracking_qa_results="sid@debian:qa-results",
            enable_regression_tracking=True,
        )

        with self.assertRaisesRegex(
            ValueError,
            '"qa_suite" is required if "enable_regression_tracking" is set',
        ):
            DebianPipelineWorkflowData(
                source_artifact=1,
                vendor="debian",
                codename="sid",
                enable_regression_tracking=True,
            )
        with self.assertRaisesRegex(
            ValueError,
            '"regression_tracking_qa_results" is required if '
            '"enable_regression_tracking" is set',
        ):
            DebianPipelineWorkflowData(
                source_artifact=1,
                vendor="debian",
                codename="sid",
                qa_suite="sid@debian:suite",
                enable_regression_tracking=True,
            )


class PackagePublishWorkflowDataTests(TestCase):
    """Tests for :py:class:`PackagePublishWorkflowData`."""

    def test_input_validation(self) -> None:
        """Test LintianInput validation."""
        PackagePublishWorkflowData(
            source_artifact=1, target_suite="bookworm@debian:suite"
        )
        PackagePublishWorkflowData(
            binary_artifacts=LookupMultiple((1,)),
            target_suite="bookworm@debian:suite",
        )
        PackagePublishWorkflowData(
            binary_artifacts=LookupMultiple((1, 2)),
            target_suite="bookworm@debian:suite",
        )

        error_msg = '"source_artifact" or "binary_artifacts" must be set'
        with self.assertRaisesRegex(ValueError, error_msg):
            PackagePublishWorkflowData(target_suite="bookworm@debian:suite")
        with self.assertRaisesRegex(ValueError, error_msg):
            PackagePublishWorkflowData(
                binary_artifacts=LookupMultiple(()),
                target_suite="bookworm@debian:suite",
            )


class ChildWorkspaceDataTests(TestCase):
    """Tests for :py:class:`ChildWorkspaceData`."""

    def test_validate_prefix(self) -> None:
        """Test prefix validation."""
        for name in [None, "test-test"]:
            with self.subTest(name=name):
                ChildWorkspaceData(prefix=name, suffix="suffix")

        for name in ["0test", "test@test", ".test", "+test"]:
            with (
                self.subTest(name=name),
                self.assertRaisesRegex(
                    ValueError, r"\bprefix\n.*String should match pattern"
                ),
            ):
                ChildWorkspaceData(prefix=name, suffix="suffix")

    def test_validate_suffix(self) -> None:
        """Test suffix validation."""
        for name in ["test-test"]:
            with self.subTest(name=name):
                ChildWorkspaceData(suffix=name)

        for name in ["0test", "test@test", ".test", "+test"]:
            with (
                self.subTest(name=name),
                self.assertRaisesRegex(
                    ValueError, r"\bsuffix\n.*String should match pattern"
                ),
            ):
                ChildWorkspaceData(suffix=name)


# Avoid running tests from common base class.
del RegressionTrackingWorkflowDataTests
