"""No document types a version or a count that the code already knows.

Nine documents carried a hand-typed contract version. Every one was stale, and
one of them was load-bearing: PLUGIN-EXTERNAL-REPO.md told an external author to
pin `>=0.13.0,<0.14.0` against a contract at 0.19.0, so a plugin built by
following the guide is refused by the loader at startup. The plugin count was
wrong in six more.

A number that is copied is a number that goes stale, and prose gives no signal
about which lines are still true. docs/PROJECT-MAP.md is generated for exactly
this, so a document points at it instead of restating it.

This test is the rule. Where a version genuinely belongs in prose - the contract
history table, a changelog entry, an ADR recording what was decided when - it is
recording the PAST, which does not go stale.
"""

import re
from pathlib import Path

import pytest

REPO = Path(__file__).resolve().parents[1]
DOCS = REPO / 'docs'

pytestmark = pytest.mark.skipif(
    not DOCS.is_dir(),
    reason='no docs/ in this checkout - it is excluded from publication')

# The map is generated and the changelog records history; both are allowed to
# carry literal versions. CONTRACT-STABILITY's table is a history of what each
# contract version DID, which is the past and stays true.
EXEMPT = {'PROJECT-MAP.md', 'CONTRACT-STABILITY.md'}

ASSIGNMENT = re.compile(r"__(?:contract_)?version__\s*=\s*['\"]([0-9]+\.[0-9]+\.[0-9]+)['\"]")
NARRATED = re.compile(
    r"__(?:contract_)?version__[^\n]{0,60}?\b([0-9]+\.[0-9]+\.[0-9]+)\b"
    r"|\b(?:contract|version) is (?:at|currently)\s+\*{0,2}([0-9]+\.[0-9]+\.[0-9]+)")
PLUGIN_COUNT = re.compile(r'\b(\d+)\s+bundled plugins\b', re.I)


def documentation_files():
    """Everything published that a reader treats as documentation.

    README.md and CONTRIBUTING.md are included deliberately: README is the first
    page anyone sees on the mirror, and it was claiming thirteen bundled plugins
    when there were fifteen. A rule that covers only docs/ leaves the most-read
    file in the repository outside it.
    """
    files = [p for p in DOCS.rglob('*.md') if p.name not in EXEMPT]
    for name in ('README.md', 'CONTRIBUTING.md'):
        top = REPO / name
        if top.is_file():
            files.append(top)
    return sorted(files)


def current_versions():
    text = (REPO / 'shopdb' / '__init__.py').read_text()
    return {
        name: re.search(r"^%s\s*=\s*['\"]([^'\"]+)['\"]" % name, text, re.M).group(1)
        for name in ('__version__', '__contract_version__')
    }


def bundled_plugin_count():
    return len(list((REPO / 'plugins').glob('*/manifest.json')))


def test_no_document_declares_a_version_literal():
    """`__contract_version__ = '0.13.0'` in prose is a promise the code breaks."""
    offenders = []
    for path in documentation_files():
        for number, line in enumerate(path.read_text(errors='replace').splitlines(), 1):
            if ASSIGNMENT.search(line):
                offenders.append('%s:%d  %s' % (path.relative_to(REPO), number, line.strip()[:90]))
    assert not offenders, (
        'These documents declare a version literal. Point at docs/PROJECT-MAP.md, '
        'which is generated:\n  ' + '\n  '.join(offenders))


def test_a_narrated_version_matches_the_code():
    """Prose that states the CURRENT version has to be right."""
    versions = set(current_versions().values())
    offenders = []
    for path in documentation_files():
        # An ADR states the version a decision was taken AT. That is a record of
        # the past, not a claim about today, and rewriting it would falsify the
        # record this project keeps ADRs for.
        if path.parent.name == 'adr':
            continue
        for number, line in enumerate(path.read_text(errors='replace').splitlines(), 1):
            for match in NARRATED.finditer(line):
                found = match.group(1) or match.group(2)
                if found and found not in versions:
                    offenders.append('%s:%d  says %s  %s'
                                     % (path.relative_to(REPO), number, found, line.strip()[:70]))
    assert not offenders, (
        'These lines state a current version that no longer matches '
        'shopdb/__init__.py:\n  ' + '\n  '.join(offenders))


def test_a_stated_plugin_count_matches_the_tree():
    actual = bundled_plugin_count()
    offenders = []
    for path in documentation_files():
        for number, line in enumerate(path.read_text(errors='replace').splitlines(), 1):
            for match in PLUGIN_COUNT.finditer(line):
                if int(match.group(1)) != actual:
                    offenders.append('%s:%d  says %s, tree has %d'
                                     % (path.relative_to(REPO), number, match.group(1), actual))
    assert not offenders, (
        'These documents count plugins by hand. The count is in '
        'docs/PROJECT-MAP.md:\n  ' + '\n  '.join(offenders))


def test_the_map_itself_is_current():
    """The pointer target has to be right, or every document pointing at it is
    wrong at one remove."""
    mapfile = DOCS / 'PROJECT-MAP.md'
    assert mapfile.is_file(), 'docs/PROJECT-MAP.md is missing; run scripts/gen_project_map.py'
    text = mapfile.read_text()
    for name, version in current_versions().items():
        assert version in text, (
            '%s is %s in the code but the generated map does not carry it. '
            'Run: venv/bin/python scripts/gen_project_map.py' % (name, version))
    assert str(bundled_plugin_count()) in text
