#!/usr/bin/env bash
# $Id: Validate 85 2026-09-05 08:27:31Z peter $
#
# Copyright (C) 2026 Peter Felecan
# Assisted by Claude Sonnet 5 (Anthropic)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Validate — project integrity checker.
#
# Verifies that critical invariants of the svah project are satisfied.
# Run before creating a delivery archive.
#
# Exit status:
#   0  — all checks passed
#   1  — one or more checks failed

set -e

PASS=0
FAIL=0
RED='\033[0;31m'
GRN='\033[0;32m'
NC='\033[0m'

check()
{
    local lDesc="${1}"
    local lResult="${2}"   # "ok" or failure message

    if [ "${lResult}" = "ok" ]
    then
        printf "${GRN}PASS${NC} %s\n" "${lDesc}"
        PASS=$((PASS + 1))
    else
        printf "${RED}FAIL${NC} %s\n     ${lResult}" "${lDesc}"
        FAIL=$((FAIL + 1))
    fi
}

file_contains()
{
    local lFile="${1}"
    local lPattern="${2}"
    grep -qF "${lPattern}" "${lFile}" 2>/dev/null && echo "ok" || \
        printf "missing in %s:\n     %s\n" "${lFile}" "${lPattern}"
}

heading_exists()
{
    local lFile="${1}"
    local lHeading="${2}"
    grep -qF "${lHeading}" "${lFile}" 2>/dev/null && echo "ok" || \
        printf "heading missing in %s:\n     %s\n" "${lFile}" "${lHeading}"
}

changelog_has_version()
{
    local lVer="${1}"
    grep -q "version ${lVer}" ChangeLog 2>/dev/null && echo "ok" || \
        printf "ChangeLog missing entry for version %s\n" "${lVer}"
}

# ------------------------------------------------------------------ #
# File existence                                                       #
# ------------------------------------------------------------------ #

for F in \
    configure.ac Makefile.am bootstrap PhysicalTest Validate CrossCompile VERSION \
    AUTHORS COPYING README NEWS ChangeLog \
    svah.1 svahsim.1 \
    Concept.org Constitution.org DevelopmentStandard.org \
    FutureResearch.org Index.org LibraryApi.org \
    ResearchJournal.org SimScriptLanguage.org TestPlan.org \
    NormalCycle.svs ErrorInjection.svs \
    PositionControl.svs PhysicalNoMotion.svs \
    CanopenSimple.h CanopenSimple.c \
    OdriveApi.h OdriveApi.c \
    HalCan.h HalCanLinux.c \
    HalTime.h HalTimeLinux.c \
    SvahLog.h SvahLog.c \
    Svah.c SvahSim.c \
    TestCanopenSimple.c TestCanopenSimpleProtocol.c \
    TestHalTime.c TestOdriveApi.c \
    TestHalCanLinux.c TestSvah.c TestSvahSim.c
do
    check "File exists: ${F}" \
        "$([ -f "${F}" ] && echo ok || printf "missing: %s\n" "${F}")"
done

# ------------------------------------------------------------------ #
# TestPlan.org — integration test sections                             #
# ------------------------------------------------------------------ #

for ID in IT-01 IT-02 IT-03 IT-04 IT-05 IT-06 IT-07 IT-08 IT-09 \
          PI-01 PI-02 PI-03 PI-04 PI-05 PI-06
do
    check "TestPlan.org has section: ${ID}" \
        "$(heading_exists TestPlan.org "** ${ID} —")"
done

# ------------------------------------------------------------------ #
# TestPlan.org — :CUSTOM_ID: properties                               #
# ------------------------------------------------------------------ #

for ID in IT-06 IT-07 IT-08 IT-09 PI-01 PI-02
do
    check "TestPlan.org CUSTOM_ID: ${ID}" \
        "$(file_contains TestPlan.org ":CUSTOM_ID: ${ID}")"
done

# ------------------------------------------------------------------ #
# Org-mode headers — broken-links:mark in all *.org               #
# ------------------------------------------------------------------ #

for F in Concept.org Constitution.org DevelopmentStandard.org \
          FutureResearch.org Index.org LibraryApi.org \
          ResearchJournal.org SimScriptLanguage.org TestPlan.org
do
    check "${F}: has broken-links:mark" \
        "$(file_contains "${F}" "broken-links:mark")"
done

# ------------------------------------------------------------------ #
# ChangeLog — version entries                                          #
# ------------------------------------------------------------------ #

VERSION=$(tail -1 VERSION)
check "ChangeLog has entry for current version ${VERSION}" \
    "$(changelog_has_version "${VERSION}")"

for VER in 0.1.0 0.1.1 0.2.0 0.2.5 0.2.6 0.3.0 0.3.3 0.3.5
do
    check "ChangeLog has entry for version ${VER}" \
        "$(changelog_has_version "${VER}")"
done

# ------------------------------------------------------------------ #
# ChangeLog/NEWS — narrative drift heuristic on the latest entry only  #
# ------------------------------------------------------------------ #
# ChangeLog and NEWS are prepend-only (DevelopmentStandard.org: "Two
# deliverables, two voices") — old entries cannot be fixed after the
# fact, so this check scans only the top (current-session) entry.
# It is a heuristic: narrative connector words in a rationale clause,
# not a guarantee of terse prose. False positives are possible; treat
# a failure as a prompt to re-read the entry, not an infallible verdict.
NARRATIVE_MARKERS='because |which explains|which means|so that |the reason |in order to|due to |root cause'

latest_changelog_entry()
{
    awk '/^[0-9]{4}-[0-9]{2}-[0-9]{2}/{n++} n<=1{print} n==2{exit}' ChangeLog
}

latest_news_entry()
{
    awk '/^svah [0-9]/{n++} n<=1{print} n==2{exit}' NEWS
}

check "ChangeLog latest entry free of narrative markers" \
    "$(latest_changelog_entry | grep -qE "${NARRATIVE_MARKERS}" && \
       printf "narrative connector found in latest ChangeLog entry\n" || echo ok)"
check "NEWS latest entry free of narrative markers" \
    "$(latest_news_entry | grep -qE "${NARRATIVE_MARKERS}" && \
       printf "narrative connector found in latest NEWS entry\n" || echo ok)"

# version.org is generated by configure from version.org.in
# It contains the REVISION macro used by all .org files via #+INCLUDE:
check "version.org exists (run ./configure)" \
    "$([ -f version.org ] && echo ok || \
       printf "missing: version.org — run ./configure\n")"

check "version.org has current version ${VERSION}" \
    "$(grep -q "${VERSION}" version.org 2>/dev/null && echo ok || \
       printf "version %s not found in version.org\n" "${VERSION}")"

# Check all .org files use #+INCLUDE: version.org (not hardcoded REVISION)
for F in Concept.org Constitution.org DevelopmentStandard.org \
          FutureResearch.org Index.org LibraryApi.org \
          ResearchJournal.org SimScriptLanguage.org TestPlan.org
do
    check "${F}: uses #+INCLUDE: version.org" \
        "$(grep -q "#+INCLUDE: version.org" "${F}" && echo ok || \
           printf "#+INCLUDE: version.org missing in %s\n" "${F}")"
done

# ------------------------------------------------------------------ #
# Makefile.am — critical variables present                             #
# ------------------------------------------------------------------ #

# ------------------------------------------------------------------ #
# Makefile.am — critical variables present (kit-based doc build)       #
# ------------------------------------------------------------------ #

check "Makefile.am drives build via batch-init.el" \
    "$(file_contains Makefile.am "batch-init.el")"
check "Makefile.am sets ORGDOC env for the kit" \
    "$(file_contains Makefile.am "ORGDOC_PDF_PROCESS_KIND")"
check "Makefile.am has html-local target" \
    "$(file_contains Makefile.am "html-local:")"
check "Makefile.am has pdf-local target" \
    "$(file_contains Makefile.am "pdf-local:")"
check "Makefile.am has man-html target" \
    "$(file_contains Makefile.am "man-html:")"
check "Makefile.am has PhysicalTest in EXTRA_DIST" \
    "$(file_contains Makefile.am "PhysicalTest")"
check "Makefile.am has PhysicalNoMotion.svs in EXTRA_DIST" \
    "$(file_contains Makefile.am "PhysicalNoMotion.svs")"
check "Makefile.am PDF_DOCS builds all nine documents" \
    "$(for d in Concept Constitution DevelopmentStandard FutureResearch \
                Index LibraryApi ResearchJournal SimScriptLanguage TestPlan; do \
         grep -q "$d.pdf" Makefile.am || printf "%s.pdf missing from PDF_DOCS\n" "$d"; \
       done; echo ok | head -1)"

# ------------------------------------------------------------------ #
# Org-mode documentation toolchain — files present at project root      #
# ------------------------------------------------------------------ #

check "batch-init.el.in present (source template)" \
    "$([ -f batch-init.el.in ] && echo ok || \
       printf "missing: batch-init.el.in\n")"
check "batch-init.el present (generated by configure)" \
    "$([ -f batch-init.el ] && echo ok || \
       printf "missing: batch-init.el — run ./configure\n")"
check "Preamble.tex present" \
    "$([ -f Preamble.tex ] && echo ok || \
       printf "missing: Preamble.tex\n")"
check "man2htmldoc present and executable (project root)" \
    "$([ -x man2htmldoc ] && echo ok || \
       printf "missing or non-executable: man2htmldoc\n")"
check "no transient tooling/ sub-directory remains" \
    "$([ -d tooling ] && printf "tooling/ should not exist\n" || echo ok)"
check "Preamble.tex and batch-init.el.in in EXTRA_DIST" \
    "$(file_contains Makefile.am "batch-init.el.in")"
check "configure.ac defines INDEX AC_ARG_VAR" \
    "$(file_contains configure.ac "AC_ARG_VAR([INDEX]")"
check "batch-init.el.in substitutes @INDEX@ (not hardcoded Index.html)" \
    "$(grep -q '@INDEX@' batch-init.el.in && \
       ! grep -q ':html-link-up.*\"Index.html\"' batch-init.el.in && echo ok || \
       printf "batch-init.el.in still hardcodes Index.html\n")"
check "no document sets a per-file HTML_LINK_UP/HOME keyword" \
    "$(grep -lE '^#\+HTML_LINK_(UP|HOME):' *.org 2>/dev/null | head -1 | grep -q . && \
       printf "a document sets HTML_LINK_UP/HOME — shadows the INDEX substitution\n" || echo ok)"
check "version.org.in has ASSISTANT macro" \
    "$(grep -q "MACRO: ASSISTANT" version.org.in && echo ok || \
       printf "ASSISTANT macro missing in version.org.in\n")"
check "PDF docs load Preamble.tex" \
    "$(grep -q 'input{Preamble.tex}' Concept.org && echo ok || \
       printf "Concept.org does not load Preamble.tex\n")"
check "assistant attribution in AUTHOR line" \
    "$(grep -q 'AUTHOR:.*ASSISTANT' Concept.org && echo ok || \
       printf "ASSISTANT not in Concept.org AUTHOR line\n")"
check "Constitution.org author is author-only (no assistant)" \
    "$(grep -q 'AUTHOR: Peter Felecan$' Constitution.org && echo ok || \
       printf "Constitution.org AUTHOR should not attribute the assistant\n")"
check "svahsim supports --bus-timeout (Option B)" \
    "$(grep -q 'bus-timeout' SvahSim.c && echo ok || \
       printf "--bus-timeout not implemented in SvahSim.c\n")"
check "svahsim supports EXPECT CONNECTED (Option A)" \
    "$(grep -q 'SIMCMD_CONNECTED' SvahSim.c && echo ok || \
       printf "SIMCMD_CONNECTED not implemented in SvahSim.c\n")"
check "BusWatch.svs present" \
    "$([ -f BusWatch.svs ] && echo ok || \
       printf "missing: BusWatch.svs\n")"
check "Makefile.am has html-export target" \
    "$(file_contains Makefile.am "html-export:")"
check "SimScriptLanguage.org documents EXPECT CONNECTED" \
    "$(grep -q 'EXPECT CONNECTED' SimScriptLanguage.org && echo ok || \
       printf "EXPECT CONNECTED not documented\n")"
check "svahsim.1 documents --bus-timeout" \
    "$(grep -q 'bus' svahsim.1 && grep -q 'timeout' svahsim.1 && echo ok || \
       printf "bus-timeout not in svahsim.1\n")"
check "no stale inline a4wide header remains" \
    "$(grep -lq 'LATEX_HEADER: .usepackage{a4wide}' *.org 2>/dev/null && \
       printf "stale inline a4wide header still present\n" || echo ok)"
check "model reference is current (Sonnet 5)" \
    "$(grep -rl 'Assisted by Claude Opus 4.8' Src Man Makefile.am \
         PhysicalTest CrossCompile bootstrap configure.ac 2>/dev/null \
       | head -1 | grep -q . && \
       printf "stale Opus 4.8 header in source/scripts\n" || echo ok)"

# Man page currency checks
check "svah.1 documents --timeout option" \
    "$(file_contains svah.1 "timeout")"
check "svah.1 documents --no-motion option" \
    "$(file_contains svah.1 "no-motion")"
check "svah.1 documents --hold option" \
    "$(file_contains svah.1 "hold")"
check "svah.1 documents --log-on-stderr option" \
    "$(grep -q 'log' svah.1 && grep -q 'stderr' svah.1 && echo ok || \
       printf "log-on-stderr not found in svah.1\n")"
check "svahsim.1 exists" \
    "$([ -f svahsim.1 ] && echo ok || printf "missing: svahsim.1\n")"

# ------------------------------------------------------------------ #
# configure.ac — optional tool checks present                          #
# ------------------------------------------------------------------ #

check "configure.ac has --without-readline support" \
    "$(file_contains configure.ac "without-readline")"

check "DSDCanRealWiringAndSwitches.png exists" \
    "$([ -f DSDCanRealWiringAndSwitches.png ] && echo ok || \
       printf "missing: DSDCanRealWiringAndSwitches.png\n")"

check "ODriveCanWiringAndSwitch.png exists" \
    "$([ -f ODriveCanWiringAndSwitch.png ] && echo ok || \
       printf "missing: ODriveCanWiringAndSwitch.png\n")"

check "configure.ac checks for emacs" \
    "$(file_contains configure.ac "AC_PATH_PROG([EMACS_BIN]")"
check "configure.ac checks for plantuml" \
    "$(file_contains configure.ac "AC_PATH_PROG([PLANTUML_BIN]")"
check "configure.ac checks for slcand" \
    "$(file_contains configure.ac "AC_PATH_PROG([SLCAND_BIN]")"

# ------------------------------------------------------------------ #
# Summary                                                              #
# ------------------------------------------------------------------ #

echo ""
echo "Results: ${PASS} passed, ${FAIL} failed"

if [ "${FAIL}" -gt 0 ]
then
    echo "Fix the above before creating a delivery archive."
    exit 1
fi
exit 0
