#!/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 license literal prefilter.

The critical property is soundness: candidate_indices(text) must be a SUPERSET of
the rules that actually match, so a prefiltered find_licenses() never misses a
detection. (The full pipeline equivalence is covered separately when the prefilter
is wired into find_licenses.)
"""

from __future__ import annotations

import json
import unittest
from pathlib import Path
from unittest.mock import patch

from decopy import license_prefilter
from decopy.licenses import clean_comments, find_licenses
from decopy.res import COMPILED_RES, LICENSES_RES

_LICENSES_DIR = Path(__file__).parent / "testdata" / "licenses"


def _rule_index(name: str) -> int:
    return next(i for i, r in enumerate(LICENSES_RES) if r.license == name)


class TestLicensePrefilter(unittest.TestCase):
    def test_available_in_this_env(self):
        # pyahocorasick is a declared optional dep; the test environment has it.
        self.assertTrue(license_prefilter.prefilter_available())
        self.assertIsInstance(license_prefilter.candidate_indices("x"), frozenset)

    def test_candidates_superset_of_actual_matches(self):
        # Soundness: every rule that matches the (cleaned) text must be a
        # candidate. This is what guarantees prefiltering loses no detection.
        files = sorted(_LICENSES_DIR.glob("*.json"))[:40]
        checked = 0
        for path in files:
            data = json.loads(path.read_text(encoding="utf-8"))
            for key in ("licenseText", "standardLicenseHeader"):
                text = data.get(key)
                if not text:
                    continue
                cleaned = clean_comments(text)
                candidates = license_prefilter.candidate_indices(cleaned)
                assert candidates is not None
                matched = {
                    i for i, pat in enumerate(COMPILED_RES) if pat.search(cleaned)
                }
                self.assertEqual(
                    matched - candidates,
                    set(),
                    f"{path.name}/{key}: matched rules missing from candidates",
                )
                checked += 1
        self.assertGreater(checked, 0)

    def test_find_licenses_prefiltered_equals_unfiltered(self):
        # The gate: prefiltered find_licenses must equal find_licenses running
        # every rule (prefilter disabled via the None fallback), over a diverse
        # corpus. Patching candidate_indices to None also exercises the fallback.
        sample = sorted(_LICENSES_DIR.glob("*.json"))[::7]
        compared = 0
        for path in sample:
            data = json.loads(path.read_text(encoding="utf-8"))
            for key in ("licenseText", "standardLicenseHeader", "licenseExceptionText"):
                text = data.get(key)
                if not text:
                    continue
                cleaned = clean_comments(text)
                prefiltered = list(find_licenses(cleaned))
                with patch("decopy.licenses.candidate_indices", return_value=None):
                    unfiltered = list(find_licenses(cleaned))
                self.assertEqual(prefiltered, unfiltered, f"{path.name}/{key}")
                compared += 1
        self.assertGreater(compared, 50)

    def test_known_literal_selects_its_rule(self):
        cpl = _rule_index("CPL")
        candidates = license_prefilter.candidate_indices(
            "Licensed under the Common Public License Version 1.0",
        )
        assert candidates is not None
        self.assertIn(cpl, candidates)

    def test_expensive_rule_skipped_for_plain_text(self):
        # An expensive family rule must not be a candidate for non-license prose.
        font = _rule_index("FontLicense")
        candidates = license_prefilter.candidate_indices(
            "the quick brown fox jumps over the lazy dog",
        )
        assert candidates is not None
        self.assertNotIn(font, candidates)

    def test_plain_text_yields_few_candidates(self):
        # Sanity: non-license text should select almost nothing (only any
        # always-run rules), proving the prefilter actually prunes.
        candidates = license_prefilter.candidate_indices("hello world\n")
        assert candidates is not None
        self.assertLess(len(candidates), 10)


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