#!/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 shipped license-text data (index snapshot, headers, texts)."""

from __future__ import annotations

import importlib.resources
import re

from decopy.licensing.data.common_licenses import COMMON_LICENSES
from decopy.licensing.data.loader import common_licenses_index
from decopy.licensing.spdx import canonical_license_name, exception_to_spdx

_COPYRIGHT_LINE = re.compile(
    r"copyright\b.*?(\(c\)|©|reserved|\b\d{4}\b|\[|<)", re.IGNORECASE
)
# A line starting a copyright notice; and a leading heading line (the licence's
# own name), recognised structurally as a short non-sentence line - mirrors
# emit-license-data.clean_license.
_COPYRIGHT_START = re.compile(r"^copyright\b", re.IGNORECASE)
_TITLE_MAX_LEN = 55


def _is_title_line(line: str) -> bool:
    stripped = line.strip()
    return 0 < len(stripped) <= _TITLE_MAX_LEN and not stripped.endswith(".")


# A whole-line angle-bracketed instruction ("<one line to give ...>"); a bare
# <url> has no internal space and is not one.
_INSTRUCTION_LINE = re.compile(r"^<[^>]*\s[^>]*>$")
# The same "how to apply" instruction without the angle brackets, as the
# LGPL headers write it ("one line to give the library's name ...").
_INSTRUCTION_PHRASE = re.compile(r"^<?one line to give\b", re.IGNORECASE)
_TEMPLATE_BLANK = re.compile(r"_{3,}")
_RESERVED_LINE = re.compile(r"^all rights reserved\.?$", re.IGNORECASE)


def _header_text(name: str) -> str:
    return (
        importlib.resources.files("decopy.licensing.data")
        .joinpath("headers", f"{name}.txt")
        .read_text(encoding="utf-8")
    )


def _header_files():
    headers = importlib.resources.files("decopy.licensing.data").joinpath("headers")
    return sorted(
        (p for p in headers.iterdir() if p.name.endswith(".txt")), key=lambda p: p.name
    )


def _license_text(name: str) -> str:
    return (
        importlib.resources.files("decopy.licensing.data")
        .joinpath("licenses", f"{name}.txt")
        .read_text(encoding="utf-8")
    )


def _license_files():
    lic = importlib.resources.files("decopy.licensing.data").joinpath("licenses")
    return sorted(
        (p for p in lic.iterdir() if p.name.endswith(".txt")), key=lambda p: p.name
    )


def _exception_files():
    exc = importlib.resources.files("decopy.licensing.data").joinpath("exceptions")
    return sorted(
        (p for p in exc.iterdir() if p.name.endswith(".txt")), key=lambda p: p.name
    )


def test_index_snapshot_nonempty():
    index = common_licenses_index()
    assert index
    assert {"GPL-2", "LGPL-2.1", "Apache-2.0"} <= index


def test_index_excludes_symlinks():
    # The GPL/LGPL/GFDL entries in /usr/share/common-licenses are symlinks to a
    # default version; the snapshot lists explicit versions only.
    index = common_licenses_index()
    assert {"GPL", "LGPL", "GFDL"}.isdisjoint(index)


def test_headers_present_for_copyleft():
    # The +/only distinction lives in the header, so the variants must differ.
    gpl2 = _header_text("GPL-2")
    gpl2_plus = _header_text("GPL-2+")
    assert gpl2.strip()
    assert gpl2_plus.strip()
    assert gpl2 != gpl2_plus


def test_gfdl_niv_header_kept_without_its_copyright_prefix():
    # GFDL packs the copyright notice and the grant on one line; the notice
    # prefix is stripped but the grant is kept (the whole line is not dropped).
    text = _header_text("GFDL-NIV-1.3")
    assert "Permission is granted" in text
    assert not _COPYRIGHT_LINE.search(text)


def test_only_gfdl_no_invariants_headers_exist():
    # The invariants-allowing GFDL notice carries unfillable "LIST ..."
    # placeholders, so only the GFDL-NIV (no-invariants) variant gets a header.
    gfdl = [f.name for f in _header_files() if f.name.startswith("GFDL")]
    assert gfdl  # some GFDL-NIV headers exist
    assert all(name.startswith("GFDL-NIV-") for name in gfdl), gfdl
    for f in _header_files():
        assert "Invariant Sections being LIST" not in f.read_text(encoding="utf-8")


def test_headers_have_no_copyright_lines():
    # The holder belongs in the Copyright field; a header must not carry a
    # copyright/template notice ("Copyright (C) <year> <name>", "Portions ...").
    for f in _header_files():
        for line in f.read_text(encoding="utf-8").splitlines():
            assert not _COPYRIGHT_LINE.search(line.strip()), (f.name, line)


def test_headers_have_no_instruction_placeholders():
    for f in _header_files():
        for line in f.read_text(encoding="utf-8").splitlines():
            stripped = line.strip()
            assert not _INSTRUCTION_LINE.match(stripped), (f.name, line)
            assert not _INSTRUCTION_PHRASE.match(stripped), (f.name, line)


def test_headers_have_no_template_blanks():
    for f in _header_files():
        for line in f.read_text(encoding="utf-8").splitlines():
            assert not _TEMPLATE_BLANK.search(line), (f.name, line)


def test_headers_have_no_all_rights_reserved_line():
    for f in _header_files():
        for line in f.read_text(encoding="utf-8").splitlines():
            assert not _RESERVED_LINE.match(line.strip()), (f.name, line)


def test_headers_not_quote_wrapped():
    for f in _header_files():
        text = f.read_text(encoding="utf-8").strip()
        assert text[:1] not in {'"', "'"}, (f.name, text[:40])
        assert text[-1:] not in {'"', "'"}, (f.name, text[-40:])


def test_headers_non_empty():
    for f in _header_files():
        assert f.read_text(encoding="utf-8").strip(), f.name


def test_seed_embedded_present():
    # The short/popular licences embedded as full verbatim text.
    for name in ("Expat", "BSD-2-clause", "BSD-3-clause", "ISC", "Zlib", "X11"):
        assert _license_text(name).strip(), name


def _exception_text(label: str) -> str:
    return (
        importlib.resources.files("decopy.licensing.data")
        .joinpath("exceptions", f"{label}.txt")
        .read_text(encoding="utf-8")
    )


_EMBEDDED_EXCEPTIONS = (
    "AutoConf",
    "AutoConf-3",
    "AutoConf-macro",
    "Bison",
    "Bootloader",
    "ClassPath",
    "Font",
    "GCC",
    "GCC-3.1",
    "LLVM",
    "Linux-syscall",
    "OCaml-LGPL",
    "PCRE2",
    "Qt-GPL-1",
    "Qt-LGPL-1.1",
)


def test_seed_exception_present():
    # The curated exceptions embedded as full verbatim text (GPL-family seed
    # plus the ranking-flagged additions).
    for label in _EMBEDDED_EXCEPTIONS:
        assert _exception_text(label).strip(), label


def _stem(path) -> str:
    return path.name[: -len(".txt")]


def test_embedded_license_stems_canonical_and_not_common():
    # The ladder is unambiguous: an embedded stem is a canonical DEP-5 name
    # (so a detected licence keys straight to it) and is not also a
    # common-licenses reference (which would win over the embedded text).
    for f in _license_files():
        stem = _stem(f)
        assert canonical_license_name(stem) == stem, f.name
        assert stem not in COMMON_LICENSES, f.name


def test_embedded_exception_stems_are_known_labels():
    # Every shipped exception file names a DEP-5 label decopy can emit.
    for f in _exception_files():
        assert exception_to_spdx(_stem(f)) is not None, f.name


def test_header_stems_canonical_or_plus_variant():
    # A header is keyed by the full variant name; the base (without a trailing
    # "+") must be a canonical DEP-5 name.
    for f in _header_files():
        base = _stem(f).removesuffix("+")
        assert canonical_license_name(base) == base, f.name


def test_embedded_keeps_license_terms():
    # The substantive terms are kept verbatim; only title/copyright are dropped.
    assert "Permission is hereby granted, free of charge" in _license_text("Expat")


def test_embedded_have_no_copyright_lines():
    # The holder belongs in the Copyright field, not the shared licence text.
    for f in _license_files():
        for line in f.read_text(encoding="utf-8").splitlines():
            assert not _COPYRIGHT_START.match(line.strip()), (f.name, line)


def test_embedded_have_no_title_line():
    # The License field already names the licence; a leading heading (e.g.
    # "MIT No Attribution", "UNICODE LICENSE V3") is redundant and must not
    # lead the embedded text.
    for f in _license_files():
        lines = [ln for ln in f.read_text(encoding="utf-8").splitlines() if ln.strip()]
        assert lines, f.name
        assert not _is_title_line(lines[0]), (f.name, lines[0])


def test_embedded_have_no_reserved_line():
    # A standalone "All rights reserved." is clerical noise, not licence terms.
    for f in _license_files():
        for line in f.read_text(encoding="utf-8").splitlines():
            assert not _RESERVED_LINE.match(line.strip()), (f.name, line)
