#!/usr/bin/env python3
# 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.

"""Tests for the holder-parsing helpers in decopy.licensing.holders."""

import unittest

from decopy.common._regex import rex
from decopy.common.core_types import CopyrightHolder, YearRange
from decopy.licensing import holders


def _triples(holder_list: list[CopyrightHolder]) -> list[tuple[str, str, str]]:
    """(name, email, years-as-string) for each holder, for readable asserts."""
    return [(h.name, h.email, str(h.years)) for h in holder_list]


class TestReSplitIter(unittest.TestCase):
    """`_re_split_iter` must split on the pattern it is given."""

    def test_splits_on_the_passed_pattern(self):
        """The separators are consumed, the between-parts are yielded."""
        pattern = rex.compile(r";")
        self.assertEqual(
            list(holders._re_split_iter(pattern, "a;b;c")),
            ["a", "b", "c"],
        )

    def test_no_match_yields_whole_text(self):
        """With no separator present the entire text comes back as one part."""
        pattern = rex.compile(r";")
        self.assertEqual(
            list(holders._re_split_iter(pattern, "abc")),
            ["abc"],
        )

    def test_uses_the_argument_not_a_hardcoded_global(self):
        """A pattern that differs from COPYRIGHT_INDICATOR_RE drives the split.

        Guards against the helper ignoring its parameter and always splitting on
        the module-level copyright-indicator regex instead.
        """
        pattern = rex.compile(r"\|")
        self.assertEqual(
            list(holders._re_split_iter(pattern, "one|two")),
            ["one", "two"],
        )


class TestGetYear(unittest.TestCase):
    """`_get_year` normalises short/2-digit years at fixed century pivots.

    Pins the pivot values so the named constants that replace the inline magic
    numbers cannot silently drift.
    """

    def test_single_digit_borrows_reference_decade(self):
        # "1990-2" -> the trailing "2" resolves to 1992 via the ref year.
        self.assertEqual(holders._get_year("2", ref=1990), 1992)

    def test_two_digit_below_pivot_maps_to_2000s(self):
        self.assertEqual(holders._get_year("49"), 2049)

    def test_two_digit_at_pivot_maps_to_1900s(self):
        self.assertEqual(holders._get_year("50"), 1950)
        self.assertEqual(holders._get_year("99"), 1999)

    def test_three_digit_is_not_a_plausible_year(self):
        self.assertIsNone(holders._get_year("500"))

    def test_four_digit_year_passes_through(self):
        self.assertEqual(holders._get_year("2023"), 2023)


class TestCopyrightHoldersAdd(unittest.TestCase):
    """CopyrightHolders.add merge-vs-store decision, per identifying signal."""

    def test_same_email_merges_regardless_of_name(self):
        collection = holders.CopyrightHolders()
        collection.add(CopyrightHolder("Alice", "a@x"))
        collection.add(CopyrightHolder("Alicia", "a@x"))
        self.assertEqual(len(collection), 1)

    def test_same_name_merges_and_adopts_missing_email(self):
        collection = holders.CopyrightHolders()
        collection.add(CopyrightHolder("Alice", ""))
        collection.add(CopyrightHolder("Alice", "a@x"))
        self.assertEqual(len(collection), 1)
        self.assertIn("a@x", collection.by_email)

    def test_same_name_different_emails_kept_separate(self):
        collection = holders.CopyrightHolders()
        collection.add(CopyrightHolder("Alice", "a1@x"))
        collection.add(CopyrightHolder("Alice", "a2@x"))
        self.assertEqual(len(collection), 2)

    def test_legal_suffix_is_ignored_when_deduping(self):
        # "Example Inc." and "Example" normalise to the same key and merge; the
        # first-seen display form is kept.
        collection = holders.CopyrightHolders()
        collection.add(CopyrightHolder("Example Inc.", ""))
        collection.add(CopyrightHolder("Example", ""))
        self.assertEqual(len(collection), 1)
        self.assertEqual(list(collection.sorted_members()), ["Example Inc."])


class TestProcessNameEmailPairs(unittest.TestCase):
    """The (name, email, years) continuation-vs-new-holder decision table."""

    def test_email_only_pair_completes_previous_holder(self):
        holder_list = [CopyrightHolder("Alice", "")]
        leftover = holders._process_name_email_pairs([("", "a@x")], holder_list, None)
        self.assertEqual(len(holder_list), 1)
        self.assertEqual(holder_list[0].email, "a@x")
        self.assertIsNone(leftover)

    def test_name_only_pair_completes_previous_holder(self):
        holder_list = [CopyrightHolder("", "a@x")]
        leftover = holders._process_name_email_pairs([("Alice", "")], holder_list, None)
        self.assertEqual(len(holder_list), 1)
        self.assertEqual(holder_list[0].name, "Alice")
        self.assertIsNone(leftover)

    def test_years_only_pair_backfills_previous_holder(self):
        previous = CopyrightHolder("Alice", "a@x")
        holder_list = [previous]
        leftover = holders._process_name_email_pairs(
            [("", "")], holder_list, YearRange(2020)
        )
        self.assertEqual(len(holder_list), 1)
        self.assertEqual(previous.years.low, 2020)
        self.assertIsNone(leftover)

    def test_complete_pair_appends_a_new_holder(self):
        holder_list: list[CopyrightHolder] = []
        leftover = holders._process_name_email_pairs(
            [("Alice", "a@x")], holder_list, None
        )
        self.assertEqual(len(holder_list), 1)
        self.assertEqual(holder_list[0].name, "Alice")
        self.assertIsNone(leftover)


class TestParseHoldersCharacterization(unittest.TestCase):
    """End-to-end `parse_holders` behaviour on realistic copyright notices.

    These pin the observable contract of the public entry point before any
    internal refactor of the notice parser. Each expectation is the parser's
    *current* output (characterization), not an independent spec; a future change
    that alters any of these is a signal to review, not necessarily a break.
    """

    def test_multiple_holders_after_one_indicator(self):
        # A single "Copyright:" indicator followed by two continuation lines
        # yields both holders (no email, no years).
        text = "Copyright:\n Maximiliano Curia\n Margarita Manterola\n"
        self.assertEqual(
            _triples(holders.parse_holders(text)),
            [("Maximiliano Curia", "", ""), ("Margarita Manterola", "", "")],
        )

    def test_years_before_holder_are_carried_onto_it(self):
        text = "Copyright 2018\n Maximiliano Curia\n"
        self.assertEqual(
            _triples(holders.parse_holders(text)),
            [("Maximiliano Curia", "", "2018")],
        )

    def test_year_after_holder_is_backfilled(self):
        text = "Copyright Maximiliano Curia (c) 2018\n"
        self.assertEqual(
            _triples(holders.parse_holders(text)),
            [("Maximiliano Curia", "", "2018")],
        )

    def test_c_comment_first_line_is_parsed(self):
        # The `/*` comment prefix on the Copyright line is stripped and the first
        # holder parsed with email and year. Current behaviour: the follow-on
        # ` * Bob ...` continuation line is NOT captured -- pinned so a future
        # NoticeParser change that starts capturing it surfaces here.
        text = "/* Copyright 2019 Alice <alice@x>\n * Bob <bob@y>\n */\n"
        self.assertEqual(
            _triples(holders.parse_holders(text)),
            [("Alice", "alice@x", "2019")],
        )

    def test_same_name_distinct_emails_stay_separate_end_to_end(self):
        text = "Copyright 2020 Alice <a1@x>\nCopyright 2021 Alice <a2@x>\n"
        self.assertEqual(
            _triples(holders.parse_holders(text)),
            [("Alice", "a1@x", "2020"), ("Alice", "a2@x", "2021")],
        )

    def test_year_range_and_email_are_parsed(self):
        text = "Copyright (C) 2015-2018 Alice <alice@example.org>\n"
        self.assertEqual(
            _triples(holders.parse_holders(text)),
            [("Alice", "alice@example.org", "2015-2018")],
        )

    def test_empty_text_has_no_holders(self):
        self.assertEqual(holders.parse_holders(""), [])

    def test_text_without_a_copyright_indicator_has_no_holders(self):
        self.assertEqual(
            holders.parse_holders("Just some text\nwith no copyright\n"), []
        )


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