#!/usr/bin/env python3

# Unit tests for the GroupingStrategy abstraction.
# Copyright: 2026, Maximiliano Curia <maxy@debian.org>
#
# License: ISC
#  Permission to use, copy, modify, and/or distribute this software for any
#  purpose with or without fee is hereby granted, provided that the above
#  copyright notice and this permission notice appear in all copies.
#  .
#  THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
#  REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
#  AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
#  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
#  LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
#  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
#  PERFORMANCE OF THIS SOFTWARE.

"""Unit tests for grouping_strategy.

These pin the strategy classes in isolation. They must agree with the
characterization tests in grouping_policy_test.py, which lock the equivalent
behaviour at the (still-live) call sites the strategies will replace.
"""

import unittest

from decopy import cmdoptions
from decopy.common import core_types
from decopy.fstree import nodes
from decopy.grouping.grouping import GroupKey
from decopy.grouping.grouping_strategy import (
    CopyrightGrouping,
    GroupingStrategy,
    LicenseGrouping,
    grouping_strategy_for,
)


def _options(*, group_by="license", split_on_license=True, split_debian=True):
    flags = ["--no-progress", "--root", "tests/testdata/one", "--group-by", group_by]
    if not split_on_license:
        flags.append("--no-split-on-license")
    if not split_debian:
        flags.append("--no-split-debian")
    return cmdoptions.process_options(flags)


class FactoryTest(unittest.TestCase):
    def test_license_option_selects_license_grouping(self):
        strategy = grouping_strategy_for("license")
        self.assertIsInstance(strategy, LicenseGrouping)

    def test_copyright_option_selects_copyright_grouping(self):
        strategy = grouping_strategy_for("copyright")
        self.assertIsInstance(strategy, CopyrightGrouping)

    def test_strategies_are_shared_singletons(self):
        a = grouping_strategy_for("license")
        b = grouping_strategy_for("license")
        self.assertIs(a, b)


class KeyCopyrightSlotTest(unittest.TestCase):
    def test_license_drops_the_copyright_slot(self):
        self.assertIsNone(LicenseGrouping().key_copyright_slot(("alice@x",)))

    def test_copyright_keeps_the_copyright_slot(self):
        self.assertEqual(
            CopyrightGrouping().key_copyright_slot(("alice@x",)), ("alice@x",)
        )


class KeepsMisplacedTest(unittest.TestCase):
    def setUp(self):
        self.group_key = GroupKey("MIT", ("alice@x",), "")
        self.group_key_no_copyright = GroupKey("MIT", (), "")

    def test_license_keeps_only_on_exact_match(self):
        strategy = LicenseGrouping()
        self.assertTrue(
            strategy.keeps_misplaced(
                self.group_key, self.group_key, self.group_key_no_copyright
            )
        )
        self.assertFalse(
            strategy.keeps_misplaced(
                GroupKey("MIT", ("bob@x",), ""),
                self.group_key,
                self.group_key_no_copyright,
            )
        )

    def test_copyright_keeps_on_exact_match(self):
        strategy = CopyrightGrouping()
        self.assertTrue(
            strategy.keeps_misplaced(
                self.group_key, self.group_key, self.group_key_no_copyright
            )
        )

    def test_copyright_rescues_uncopyrighted_file(self):
        # An empty copyright slot matches the no-copyright key -> kept.
        strategy = CopyrightGrouping()
        self.assertTrue(
            strategy.keeps_misplaced(
                self.group_key_no_copyright,
                self.group_key,
                self.group_key_no_copyright,
            )
        )

    def test_copyright_drops_mismatched_nonempty_copyright(self):
        strategy = CopyrightGrouping()
        self.assertFalse(
            strategy.keeps_misplaced(
                GroupKey("MIT", ("bob@x",), ""),
                self.group_key,
                self.group_key_no_copyright,
            )
        )

    def test_copyright_keeps_file_whose_holders_are_a_subset(self):
        # A multi-holder paragraph's Copyright is the union of its files'
        # holders, so each file legitimately carries a subset. A file is kept
        # when its holders are a subset of the group's declared holders.
        strategy = CopyrightGrouping()
        union_group = GroupKey("MIT", ("alice@x", "bob@x"), "")
        self.assertTrue(
            strategy.keeps_misplaced(
                GroupKey("MIT", ("alice@x",), ""), union_group, GroupKey("MIT", (), "")
            )
        )

    def test_copyright_drops_file_with_an_undeclared_holder(self):
        # A holder the paragraph does not declare is not a subset -> removed
        # (the paragraph's Copyright is stale relative to the file).
        strategy = CopyrightGrouping()
        union_group = GroupKey("MIT", ("alice@x", "bob@x"), "")
        self.assertFalse(
            strategy.keeps_misplaced(
                GroupKey("MIT", ("alice@x", "carol@x"), ""),
                union_group,
                GroupKey("MIT", (), ""),
            )
        )

    def test_copyright_drops_subset_when_license_differs(self):
        # Subset applies to the copyright slot only; a different license (or
        # path) still removes the file.
        strategy = CopyrightGrouping()
        self.assertFalse(
            strategy.keeps_misplaced(
                GroupKey("BSD", ("alice@x",), ""),
                GroupKey("MIT", ("alice@x", "bob@x"), ""),
                GroupKey("MIT", (), ""),
            )
        )


class ReportsCopyrightDriftTest(unittest.TestCase):
    """Only license grouping reports copyright drift.

    Under copyright grouping a holder change already surfaces as a relicensed
    entry (the copyright slot participates in the key), so reporting drift there
    would double-count.
    """

    def test_license_grouping_reports_drift(self):
        self.assertTrue(LicenseGrouping().reports_copyright_drift())

    def test_copyright_grouping_does_not_report_drift(self):
        self.assertFalse(CopyrightGrouping().reports_copyright_drift())


class SplittingPathTest(unittest.TestCase):
    """The shared base-class splitting_path matches file_policy.splitting_path."""

    def setUp(self):
        self.strategy = LicenseGrouping()
        self.root = nodes.DirInfo(parent=None, name="")
        self.src = nodes.DirInfo(parent=self.root, name="src")
        self.root.file_list["src"] = self.src
        self.debian = nodes.DirInfo(parent=self.root, name="debian")
        self.root.file_list["debian"] = self.debian
        self.lic = nodes.DirInfo(parent=self.src, name="lic")
        self.lic.add_licenses(["MIT"])
        self.src.file_list["lic"] = self.lic

    def test_root_returns_its_own_name(self):
        self.assertEqual(self.strategy.splitting_path(self.root, _options()), "")

    def test_license_dir_with_split_on_license(self):
        opts = _options(split_on_license=True)
        self.assertEqual(self.strategy.splitting_path(self.lic, opts), "src/lic")

    def test_license_dir_recurses_without_split_on_license(self):
        opts = _options(split_on_license=False)
        self.assertEqual(self.strategy.splitting_path(self.lic, opts), "")

    def test_debian_dir_with_split_debian(self):
        opts = _options(split_debian=True)
        self.assertEqual(self.strategy.splitting_path(self.debian, opts), "debian")

    def test_debian_dir_recurses_without_split_debian(self):
        opts = _options(split_debian=False)
        self.assertEqual(self.strategy.splitting_path(self.debian, opts), "")

    def test_file_delegates_to_parent(self):
        f = nodes.FileInfo(parent=self.lic, name="a.txt")
        self.assertEqual(
            self.strategy.splitting_path(f, _options(split_on_license=True)),
            "src/lic",
        )

    def test_parentless_file_returns_none(self):
        orphan = nodes.FileInfo(parent=None, name="o.txt")
        self.assertIsNone(self.strategy.splitting_path(orphan, _options()))


class SortBandTest(unittest.TestCase):
    def setUp(self):
        self.strategy = LicenseGrouping()

    def test_normal_group_is_band_zero(self):
        band = self.strategy.sort_band("MIT", "src", split_debian=True)
        self.assertEqual(band, 0)

    def test_debian_group_is_band_two_with_split_debian(self):
        band = self.strategy.sort_band("MIT", "debian", split_debian=True)
        self.assertEqual(band, 2)

    def test_debian_group_is_band_zero_without_split_debian(self):
        band = self.strategy.sort_band("MIT", "debian", split_debian=False)
        self.assertEqual(band, 0)

    def test_unknown_license_is_band_one(self):
        band = self.strategy.sort_band(core_types.UNKNOWN, "src", split_debian=True)
        self.assertEqual(band, 1)

    def test_unknown_overrides_debian_band(self):
        band = self.strategy.sort_band(
            core_types.UNKNOWN_COPYRIGHTED, "debian", split_debian=True
        )
        self.assertEqual(band, 1)


class AbstractContractTest(unittest.TestCase):
    def test_base_class_cannot_be_instantiated(self):
        with self.assertRaises(TypeError):
            GroupingStrategy()  # type: ignore[abstract]


if __name__ == "__main__":
    unittest.main()
