#!/usr/bin/env python3

#
# man-test.py - test exapmles in a man page
#
# Copyright (C) 2021 Masatake YAMATO
#
# 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 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

#
# Python 3.5 or later is required.
# On Windows, unix-like shell (e.g. bash) and diff command are needed.
#

import sys
import re
import os
import subprocess
import copy

def print_usage(n, f):
    print ('Usage: man-test.py TMPDIR CTAGS ctags-lang-<LANG>.7.rst.in...', file=f)
    sys.exit(n)

def next_segment(line):
    if line.endswith ('\\'):
        return line[0:-1]
    else:
        return (line + '\n')

def wash_cmdline(cmdline):
    return cmdline

def verify_test_case(t):
    prefix = '%(man_file)s[%(nth)d]:%(start_linum)d: '%t
    msg = False
    if not 'code' in t:
        msg = 'cannot find input lines'
    elif not 'tags' in t:
        msg = 'cannot find expected tags output'
    elif not 'cmdline' in t:
        msg = 'cannot find ctags command line'

    if msg:
        msg = prefix + msg
    return msg

def is_option(c):
    if re.search('--[a-z_].*', c):
        return True
    elif re.search('^-[a-z]$', c):
        return True
    return False

def run_test_case(tmpdir, ctags, t):
    d = tmpdir + '/' + str(os.getpid())
    os.makedirs (d,exist_ok=True)
    i = d + '/' + t['input_file_name']
    o0 = 'actual.tags'
    o = d + '/' + o0
    e0 = 'expected.tags'
    e = d + '/' + e0
    D = d + '/' + 'tags.diff'
    O0 = 'args.ctags'
    O = d + '/' + O0
    with open(i, mode='w', encoding='utf-8') as f:
        f.write(t['code'])

    with open(e, mode='w', encoding='utf-8') as g:
        g.write(t['tags'])

    inputf=None
    with open(O, mode='w', encoding='utf-8') as Of:
        in_pattern = False
        for c in t['cmdline'].split():
            if c == '--options=NONE':
                continue
            elif c.startswith('input.'):
                inputf = c
                continue
            elif c.startswith('--regex-'):
                in_pattern = c
            elif in_pattern and not is_option(c):
                # TODO: This doesn't work if whitespace is repeated.
                in_pattern = in_pattern + ' ' + c
            else:
                if in_pattern:
                    print (in_pattern, file=Of)
                    in_pattern = False
                print (c, file=Of)
        if in_pattern:
            print (in_pattern, file=Of)

    with open(o, mode='w', encoding='utf-8') as h:
        cmdline = [ctags, '--quiet', '--options=NONE',
                   '--options=' + O0, inputf]
        subprocess.run(cmdline, cwd=d, stdout=h)

    with open(D, mode='w', encoding='utf-8') as diff:
        r = subprocess.run(['diff', '-uN', '--strip-trailing-cr', o0, e0],
                           cwd=d, stdout=diff).returncode

    if r == 0:
        t['result'] = True
        t['result_readable'] = 'passed'
    else:
        with open(o, encoding='utf-8') as f:
            t['actual_tags'] = f.read()
        t['result'] = False
        t['result_readable'] = 'failed'
        with open(D, encoding='utf-8') as diff:
            t['tags_diff'] = diff.read()
    os.remove(O)
    os.remove(i)
    os.remove(e)
    os.remove(o)
    os.remove(D)
    os.rmdir(d)
    return t

def report_result(r):
    print ('%(man_file)s[%(nth)d]:%(start_linum)d...%(result_readable)s'%r)

def report_failure(r):
    print ('## %(man_file)s[%(nth)d]:%(start_linum)d'%r)
    print ('### input')
    print ('```')
    print (r['code'])
    print ('```')
    print ('### cmdline')
    print ('```')
    print (r['cmdline'])
    print ('```')
    print ('### expected tags')
    print ('```')
    print (r['tags'])
    print ('```')
    print ('### actual tags')
    print ('```')
    print (r['actual_tags'])
    print ('```')
    print ('### diff of tag files')
    print ('```')
    print (r['tags_diff'])
    print ('```')

class state:
    start = 0
    tags  = 1
    code  = 2
    code_done = 3
    input = 4
    output = 5
    output_after_options = 6

def extract_test_cases(f):
    linum=0
    nth=0
    s=state.start
    test_spec = {}

    for line in  f.readlines():
        linum += 1
        line = line.rstrip('\r\n')

        if s == state.tags or s == state.code:
            if prefix:
                m = re.search('^' + prefix + '(.*)$', line)
                if m:
                    sink += next_segment(m.group(1))
                    continue
                if line == '':
                    sink += '\n'
                    continue
            else:
                m = re.search('^([ \t]+)(.+)$', line)
                if m:
                    prefix = m.group(1)
                    sink += next_segment(m.group(2))
                    continue
                elif re.search ('^([ \t]*)$', line):
                    continue

            sink = sink.rstrip('\r\n') + '\n'

            if s == state.code:
                test_spec['code'] = sink
                s = state.code_done
            else:
                test_spec['tags'] = sink
                test_spec['nth'] = nth
                nth += 1
                test_spec['end_linum'] = linum
                s = state.start
                yield test_spec

        m = s == state.start and re.search ('^"(input\\.[^"]+)"$', line)
        if m:
            test_spec ['start_linum'] = linum
            test_spec ['input_file_name'] = m.group(1)
            s = state.input
            continue
        m = s == state.input and re.search ('^.. code-block::.*', line)
        if m:
            sink = ''
            prefix = False
            s = state.code
            continue
        m = s == state.code_done and re.search ('^"output.tags"$', line)
        if m:
            s = state.output
            continue
        m = s == state.output and re.search ('with[ \t]"([^"]+)"', line)
        if m:
            test_spec ['cmdline'] = wash_cmdline (m.group(1))
            s = state.output_after_options
            continue
        if s == state.output_after_options \
           and (line == "::" or re.search ('^.. code-block:: *tags$', line)):
            sink = ''
            prefix = False
            s = state.tags
            continue

def extract_features (f):
    for line in  f.readlines():
        line = line.rstrip('\r\n')
        m = re.search(':Expected feature:[ \t]*([a-zA-Z0-9-_]+)', line)
        if m:
            yield m.group(1)
        continue

# Taken from misc/units.py
def has_feature (ctags, feat):
    ret = subprocess.run([ctags, '--quiet', '--options=NONE',
                          '--list-features', '--with-list=no'],
                         stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
    if feat in re.sub(r'(?m)^([^ ]+).*$', r'\1',
                      ret.stdout.decode('utf-8')).splitlines():
        return True
    return False

def report_skip(man_file, feat):
    print ('%s[feat]:%s...skipped' % (man_file, feat))

def man_test (tmpdir, ctags, man_file):
    failures = []
    result = True
    print ('# Run test cases in ' + man_file)
    print ('```')
    with open(man_file, encoding='utf-8') as f:
        for x in extract_features (f):
            if not has_feature(ctags, x):
                report_skip(man_file, x)
                print ('```')
                return True
        f.seek(0)
        for t in extract_test_cases (f):
            t['man_file'] = man_file
            v = verify_test_case (t)
            if v:
                print ("error: " + v, file=sys.stderr)
                result = False
                continue
            r = run_test_case (tmpdir, ctags, t)
            report_result (r)
            if not r['result']:
                result = False
                failures.append(copy.copy(r))
    print ('```')
    if (len(failures) > 0):
        print ('# Failed test case(s)')
        for f in failures:
            report_failure(f)
    return result

def man_tests (tmpdir, ctags, man_files):
    result = 0
    for m in man_files:
        if not man_test(tmpdir, ctags, m):
            result += 1
    print ('OK' if result == 0 else 'FAILED')
    return result == 0

if (len(sys.argv) < 4) or (sys.argv[1] == '-h' or sys.argv[1] == '--help'):
    print_usage (2, sys.stderr)

tmpdir = sys.argv[1]
ctags = os.path.abspath(sys.argv[2])
sys.exit(0 if man_tests (tmpdir, ctags, sys.argv[3:]) else 1)
